mirror of
https://github.com/chatmail/core.git
synced 2026-07-23 03:13:06 +03:00
Compare commits
3 Commits
v1.155.4
...
simon/bett
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cbc1c3eb8 | ||
|
|
cbc79cdcc5 | ||
|
|
3b6fb20e11 |
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.84.1
|
||||
RUSTUP_TOOLCHAIN: 1.84.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -97,15 +97,15 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.84.1
|
||||
rust: 1.84.0
|
||||
- os: windows-latest
|
||||
rust: 1.84.1
|
||||
rust: 1.84.0
|
||||
- os: macos-latest
|
||||
rust: 1.84.1
|
||||
rust: 1.84.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.81.0
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
rust: 1.81.0
|
||||
rust: 1.77.0
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/dependabot.yml
vendored
2
.github/workflows/dependabot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v2.3.0
|
||||
uses: dependabot/fetch-metadata@v2.2.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Approve a PR
|
||||
|
||||
202
CHANGELOG.md
202
CHANGELOG.md
@@ -1,198 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.155.4] - 2025-02-10
|
||||
|
||||
### CI
|
||||
|
||||
- Upgrade Rust from 1.84.0 to 1.84.1.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Use CRLF newlines in vCards.
|
||||
- Make vCard parsing more robust in case of trailing newlines.
|
||||
- Do not include CRLF before MIME boundary in the part body.
|
||||
- Accept QR codes with 'broken' JSON ([#6528](https://github.com/deltachat/deltachat-core-rust/pull/6528)).
|
||||
|
||||
### Other
|
||||
|
||||
- Add `MessageQuote.chat_id`.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move even more tests into their own files ([#6521](https://github.com/deltachat/deltachat-core-rust/pull/6521)).
|
||||
|
||||
## [1.155.3] - 2025-02-05
|
||||
|
||||
### Fixes
|
||||
|
||||
- Store device token in IMAP METADATA on each connection.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Upgrade iroh from 0.30 to 0.32.
|
||||
- Update `pgp` to 0.15.
|
||||
- cargo: Bump thiserror from 1.0.69 to 2.0.9.
|
||||
- cargo: Bump pin-project from 1.1.7 to 1.1.8.
|
||||
- cargo: Bump dirs from 5.0.1 to 6.0.0.
|
||||
- cargo: Bump hyper from 1.5.2 to 1.6.0.
|
||||
- cargo: Bump webpki-roots from 0.26.7 to 0.26.8.
|
||||
- cargo: Bump futures-lite from 2.5.0 to 2.6.0.
|
||||
- Update OpenSSL to fix RUSTSEC-2025-0004.
|
||||
- cargo: Bump tokio from 1.42.0 to 1.43.0.
|
||||
- cargo: Bump syn from 2.0.94 to 2.0.98.
|
||||
- cargo: Bump rustls from 0.23.20 to 0.23.22.
|
||||
- cargo: Bump data-encoding from 2.6.0 to 2.7.0.
|
||||
- cargo: Bump serde_json from 1.0.134 to 1.0.138.
|
||||
- cargo: Bump uuid from 1.11.0 to 1.12.1.
|
||||
- cargo: Bump log from 0.4.22 to 0.4.25.
|
||||
- cargo: Bump rustls-pki-types from 1.10.1 to 1.11.0.
|
||||
- Update futures-concurrency.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Assign docs to correct object.
|
||||
|
||||
### Tests
|
||||
|
||||
- Make sure DCBACKUP2 compatibility does not break again.
|
||||
|
||||
## [1.155.2] - 2025-01-31
|
||||
|
||||
This release accidentally broke compatibility
|
||||
with previous versions of `DCBACKUP2` QR codes
|
||||
due to iroh upgrade.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `IncomingReaction.chat_id` ([#6459](https://github.com/deltachat/deltachat-core-rust/pull/6459)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Deduplicate blob files in `chat.rs`, `config.rs`, and `integration.rs`.
|
||||
- Improve logging around IMAP IDLE.
|
||||
- Upgrade to iroh@0.30.0.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't remove file extension when recoding avatars.
|
||||
- Use `BufReader` when reading .xdc files.
|
||||
- No implicit member changes when we are added to the group ([#6493](https://github.com/deltachat/deltachat-core-rust/pull/6493)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- jsonrpc: Update documentation for `select_account` and `get_selected_account_id` ([#6483](https://github.com/deltachat/deltachat-core-rust/pull/6483)).
|
||||
- jsonrpc: Add docs for some functions.
|
||||
|
||||
## [1.155.1] - 2025-01-25
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Only accept SetContacts sync messages for broadcast lists.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't create tombstones when synchronizing broadcast list members.
|
||||
- Use non-empty `To:` field for "saved messages".
|
||||
- Only send Chat-Group-Member-Timestamps in groups.
|
||||
- Use 0 timestamps if Chat-Group-Member-Timestamps is not set.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove BlobObject::create(), use create_and_deduplicate_from_bytes() instead ([#6467](https://github.com/deltachat/deltachat-core-rust/pull/6467)).
|
||||
- Move more tests into their own files ([#6473](https://github.com/deltachat/deltachat-core-rust/pull/6473)).
|
||||
|
||||
## [1.155.0] - 2025-01-23
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add JSON-RPC API to get past members.
|
||||
|
||||
### Build system
|
||||
|
||||
- Update Rust.
|
||||
- Increase MSRV to 1.81.0
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- feat: Set BccSelf to true when receiving a sync message ([#6434](https://github.com/deltachat/deltachat-core-rust/pull/6434))
|
||||
- File deduplication ([#6332](https://github.com/deltachat/deltachat-core-rust/pull/6332))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move tests to their own files.
|
||||
- Extract `group_changes_msgs()` function ([#6460](https://github.com/deltachat/deltachat-core-rust/pull/6460)).
|
||||
|
||||
## [1.154.3] - 2025-01-20
|
||||
|
||||
### Build system
|
||||
|
||||
- Remove encoded-words from flake.nix.
|
||||
- nix: Update rust-email hash in flake.nix.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Remove unused function delete_files_in_dir() ([#6454](https://github.com/deltachat/deltachat-core-rust/pull/6454)).
|
||||
|
||||
## [1.154.2] - 2025-01-20
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add API to save messages ([#5606](https://github.com/deltachat/deltachat-core-rust/pull/5606)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- fix: Don't accidentally remove Self from groups ([#6455](https://github.com/deltachat/deltachat-core-rust/pull/6455)).
|
||||
- Do not create tombstones for members removed from unpromoted groups.
|
||||
|
||||
### Build system
|
||||
|
||||
- Switch to non-git version of encoded-words.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Make memberlist update logic easier to follow.
|
||||
|
||||
## [1.154.1] - 2025-01-15
|
||||
|
||||
### Tests
|
||||
|
||||
- Expect trashing of no-op "member added" in non_member_cannot_modify_member_list.
|
||||
|
||||
## [1.154.0] - 2025-01-15
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- New group consistency algorithm.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Migration: Set bcc_self=1 if it's unset and delete_server_after!=1 ([#6432](https://github.com/deltachat/deltachat-core-rust/pull/6432)).
|
||||
- Clear the config cache after every migration ([#6438](https://github.com/deltachat/deltachat-core-rust/pull/6438)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Increase minimum supported Python version to 3.8.
|
||||
- [**breaking**] Remove jsonrpc feature flag.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.84.0.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Beta Clippy suggestions ([#6422](https://github.com/deltachat/deltachat-core-rust/pull/6422)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use let..else.
|
||||
- Add why_cant_send_ex() capable to only ignore specified conditions.
|
||||
- Remove unnecessary is_contact_in_chat check.
|
||||
- Eliminate remaining repeat_vars() calls ([#6359](https://github.com/deltachat/deltachat-core-rust/pull/6359)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Use assert_eq! to compare chatlist length.
|
||||
|
||||
## [1.153.0] - 2025-01-05
|
||||
|
||||
### Features / Changes
|
||||
@@ -5778,12 +5585,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.152.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.0..v1.152.1
|
||||
[1.152.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.1..v1.152.2
|
||||
[1.153.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.152.2..v1.153.0
|
||||
[1.154.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.153.0..v1.154.0
|
||||
[1.154.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.0..v1.154.1
|
||||
[1.154.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.1..v1.154.2
|
||||
[1.154.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.2..v1.154.3
|
||||
[1.155.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.154.3..v1.155.0
|
||||
[1.155.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.0..v1.155.1
|
||||
[1.155.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.1..v1.155.2
|
||||
[1.155.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.2..v1.155.3
|
||||
[1.155.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.155.3..v1.155.4
|
||||
|
||||
1494
Cargo.lock
generated
1494
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
28
Cargo.toml
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.155.4"
|
||||
version = "1.153.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.81"
|
||||
rust-version = "1.77"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[profile.dev]
|
||||
@@ -49,27 +49,26 @@ base64 = { workspace = true }
|
||||
brotli = { version = "7", default-features=false, features = ["std"] }
|
||||
bytes = "1"
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
data-encoding = "2.7.0"
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = "0.2"
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "0.10"
|
||||
fd-lock = "4"
|
||||
futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "=0.25.0-alpha.4"
|
||||
hickory-resolver = "=0.25.0-alpha.2"
|
||||
http-body-util = "0.1.2"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
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.32", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.32", default-features = false }
|
||||
iroh-gossip = { version = "0.28.1", default-features = false, features = ["net"] }
|
||||
iroh-net = { version = "0.28.1", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = { workspace = true }
|
||||
mailparse = "0.16"
|
||||
mailparse = "0.15"
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
@@ -77,7 +76,7 @@ num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.15.0", default-features = false }
|
||||
pgp = { version = "0.14.2", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.37"
|
||||
@@ -86,8 +85,8 @@ rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustls-pki-types = "1.11.0"
|
||||
rustls = { version = "0.23.22", default-features = false }
|
||||
rustls-pki-types = "1.10.1"
|
||||
rustls = { version = "0.23.20", default-features = false }
|
||||
sanitize-filename = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
@@ -110,8 +109,7 @@ tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
webpki-roots = "0.26.8"
|
||||
blake3 = "1.5.5"
|
||||
webpki-roots = "0.26.7"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
@@ -176,7 +174,7 @@ deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.31"
|
||||
futures-lite = "2.6.0"
|
||||
futures-lite = "2.5.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
nu-ansi-term = "0.46"
|
||||
@@ -189,7 +187,7 @@ sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.14.0"
|
||||
thiserror = "2"
|
||||
thiserror = "1"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.13"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
@@ -78,21 +78,21 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
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"
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
EMAIL:{addr}\n\
|
||||
FN:{display_name}\n"
|
||||
);
|
||||
if let Some(key) = &c.key {
|
||||
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
|
||||
res += &format!("KEY:data:application/pgp-keys;base64,{key}\n");
|
||||
}
|
||||
if let Some(profile_image) = &c.profile_image {
|
||||
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
|
||||
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\n");
|
||||
}
|
||||
if let Some(timestamp) = format_timestamp(c) {
|
||||
res += &format!("REV:{timestamp}\r\n");
|
||||
res += &format!("REV:{timestamp}\n");
|
||||
}
|
||||
res += "END:VCARD\r\n";
|
||||
res += "END:VCARD\n";
|
||||
}
|
||||
res
|
||||
}
|
||||
@@ -206,21 +206,22 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
contacts
|
||||
@@ -539,30 +540,6 @@ END:VCARD",
|
||||
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 = [
|
||||
@@ -582,20 +559,20 @@ END:VCARD\r
|
||||
},
|
||||
];
|
||||
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",
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
EMAIL:alice@example.org\n\
|
||||
FN:Alice Wonderland\n\
|
||||
KEY:data:application/pgp-keys;base64,[base64-data]\n\
|
||||
PHOTO:data:image/jpeg;base64,image in Base64\n\
|
||||
REV:20240418T184242Z\n\
|
||||
END:VCARD\n",
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
EMAIL:bob@example.com\n\
|
||||
FN:bob@example.com\n\
|
||||
REV:19700101T000000Z\n\
|
||||
END:VCARD\n",
|
||||
];
|
||||
let mut expected = "".to_string();
|
||||
for len in 0..=contacts.len() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.155.4"
|
||||
version = "1.153.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1974,36 +1974,6 @@ void dc_delete_msgs (dc_context_t* context, const uint3
|
||||
void dc_forward_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Save a copy of messages in "Saved Messages".
|
||||
*
|
||||
* In contrast to forwarding messages,
|
||||
* information as author, date and origin are preserved.
|
||||
* The action completes locally, so "Saved Messages" do not show sending errors in case one is offline.
|
||||
* Still, a sync message is emitted, so that other devices will save the same message,
|
||||
* as long as not deleted before.
|
||||
*
|
||||
* To check if a message was saved, use dc_msg_get_saved_msg_id(),
|
||||
* UI may show an indicator and offer an "Unsave" instead of a "Save" button then.
|
||||
*
|
||||
* The other way round, from inside the "Saved Messages" chat,
|
||||
* UI may show the indicator and "Unsave" button checking dc_msg_get_original_msg_id()
|
||||
* and offer a button to go the original message.
|
||||
*
|
||||
* "Unsave" is done by deleting the saved message.
|
||||
* Webxdc updates are not copied on purpose.
|
||||
*
|
||||
* For performance reasons, esp. when saving lots of messages,
|
||||
* UI should call this function from a background thread.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_ids An array of uint32_t containing all message IDs that should be saved.
|
||||
* @param msg_cnt The number of messages IDs in the msg_ids array.
|
||||
*/
|
||||
void dc_save_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||
|
||||
|
||||
/**
|
||||
* Resend messages and make information available for newly added chat members.
|
||||
* Resending sends out the original message, however, recipients and webxdc-status may differ.
|
||||
@@ -4752,36 +4722,10 @@ void dc_msg_set_override_sender_name(dc_msg_t* msg, const char* name)
|
||||
* @param file If the message object is used in dc_send_msg() later,
|
||||
* this must be the full path of the image file to send.
|
||||
* @param filemime The MIME type of the file. NULL if you don't know or don't care.
|
||||
* @deprecated 2025-01-21 Use dc_msg_set_file_and_deduplicate instead
|
||||
*/
|
||||
void dc_msg_set_file (dc_msg_t* msg, const char* file, const char* filemime);
|
||||
|
||||
|
||||
/**
|
||||
* Sets the file associated with a message.
|
||||
*
|
||||
* If `name` is non-null, it is used as the file name
|
||||
* and the actual current name of the file is ignored.
|
||||
*
|
||||
* If the source file is already in the blobdir, it will be renamed,
|
||||
* otherwise it will be copied to the blobdir first.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object. Must not be NULL.
|
||||
* @param file The path of the file to attach. Must not be NULL.
|
||||
* @param name The original filename of the attachment. If NULL, the current name of `file` will be used instead.
|
||||
* @param filemime The MIME type of the file. NULL if you don't know or don't care.
|
||||
*/
|
||||
void dc_msg_set_file_and_deduplicate(dc_msg_t* msg, const char* file, const char* name, const char* filemime);
|
||||
|
||||
|
||||
/**
|
||||
* Set the dimensions associated with message object.
|
||||
* Typically this is the width and the height of an image or video associated using dc_msg_set_file().
|
||||
@@ -4924,35 +4868,6 @@ dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
|
||||
dc_msg_t* dc_msg_get_parent (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get original message ID for a saved message from the "Saved Messages" chat.
|
||||
*
|
||||
* Can be used by UI to show a button to go the original message
|
||||
* and an option to "Unsave" the message.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object. Usually, this refers to a a message inside "Saved Messages".
|
||||
* @return The message ID of the original message.
|
||||
* 0 if the given message object is not a "Saved Message"
|
||||
* or if the original message does no longer exist.
|
||||
*/
|
||||
uint32_t dc_msg_get_original_msg_id (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a message was saved and return its ID inside "Saved Messages".
|
||||
*
|
||||
* Deleting the returned message will un-save the message.
|
||||
* The state "is saved" can be used to show some icon to indicate that a message was saved.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object. Usually, this refers to a a message outside "Saved Messages".
|
||||
* @return The message ID inside "Saved Messages", if any.
|
||||
* 0 if the given message object is not saved.
|
||||
*/
|
||||
uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Force the message to be sent in plain text.
|
||||
*
|
||||
|
||||
@@ -1979,26 +1979,6 @@ pub unsafe extern "C" fn dc_forward_msgs(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_save_msgs(
|
||||
context: *mut dc_context_t,
|
||||
msg_ids: *const u32,
|
||||
msg_cnt: libc::c_int,
|
||||
) {
|
||||
if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 {
|
||||
eprintln!("ignoring careless call to dc_save_msgs()");
|
||||
return;
|
||||
}
|
||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
chat::save_msgs(ctx, &msg_ids[..])
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "Failed to save message")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_resend_msgs(
|
||||
context: *mut dc_context_t,
|
||||
@@ -3835,33 +3815,6 @@ pub unsafe extern "C" fn dc_msg_set_file(
|
||||
)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_set_file_and_deduplicate(
|
||||
msg: *mut dc_msg_t,
|
||||
file: *const libc::c_char,
|
||||
name: *const libc::c_char,
|
||||
filemime: *const libc::c_char,
|
||||
) {
|
||||
if msg.is_null() || file.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_set_file_and_deduplicate()");
|
||||
return;
|
||||
}
|
||||
let ffi_msg = &mut *msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
ffi_msg
|
||||
.message
|
||||
.set_file_and_deduplicate(
|
||||
ctx,
|
||||
as_path(file),
|
||||
to_opt_string_lossy(name).as_deref(),
|
||||
to_opt_string_lossy(filemime).as_deref(),
|
||||
)
|
||||
.context("Failed to set file")
|
||||
.log_err(&*ffi_msg.context)
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_set_dimension(
|
||||
msg: *mut dc_msg_t,
|
||||
@@ -4027,48 +3980,6 @@ pub unsafe extern "C" fn dc_msg_get_parent(msg: *const dc_msg_t) -> *mut dc_msg_
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_original_msg_id(msg: *const dc_msg_t) -> u32 {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_original_msg_id()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg: &MessageWrapper = &*msg;
|
||||
let context = &*ffi_msg.context;
|
||||
block_on(async move {
|
||||
ffi_msg
|
||||
.message
|
||||
.get_original_msg_id(context)
|
||||
.await
|
||||
.context("failed to get original message")
|
||||
.log_err(context)
|
||||
.unwrap_or_default()
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_saved_msg_id(msg: *const dc_msg_t) -> u32 {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_saved_msg_id()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg: &MessageWrapper = &*msg;
|
||||
let context = &*ffi_msg.context;
|
||||
block_on(async move {
|
||||
ffi_msg
|
||||
.message
|
||||
.get_saved_msg_id(context)
|
||||
.await
|
||||
.context("failed to get original message")
|
||||
.log_err(context)
|
||||
.unwrap_or_default()
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_force_plaintext(msg: *mut dc_msg_t) {
|
||||
if msg.is_null() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.155.4"
|
||||
version = "1.153.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -49,7 +49,7 @@ use types::chat::FullChat;
|
||||
use types::contact::{ContactObject, VcardContact};
|
||||
use types::events::Event;
|
||||
use types::http::HttpResponse;
|
||||
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||
use types::message::{MessageData, MessageObject, MessageReadReceipt, QuotedText};
|
||||
use types::provider_info::ProviderInfo;
|
||||
use types::reactions::JSONRPCReactions;
|
||||
use types::webxdc::WebxdcMessageInfo;
|
||||
@@ -212,12 +212,14 @@ impl CommandApi {
|
||||
self.accounts.read().await.get_all()
|
||||
}
|
||||
|
||||
/// Select account in account manager, this saves the last used account to accounts.toml
|
||||
/// Select account id for internally selected state.
|
||||
/// TODO: Likely this is deprecated as all methods take an account id now.
|
||||
async fn select_account(&self, id: u32) -> Result<()> {
|
||||
self.accounts.write().await.select_account(id).await
|
||||
}
|
||||
|
||||
/// Get the selected account from the account manager (on startup it is read from accounts.toml)
|
||||
/// Get the selected account id of the internal state..
|
||||
/// TODO: Likely this is deprecated as all methods take an account id now.
|
||||
async fn get_selected_account_id(&self) -> Option<u32> {
|
||||
self.accounts.read().await.get_selected_account_id()
|
||||
}
|
||||
@@ -834,13 +836,6 @@ impl CommandApi {
|
||||
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
|
||||
}
|
||||
|
||||
/// Returns contact IDs of the past chat members.
|
||||
async fn get_past_chat_contacts(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contacts = chat::get_past_chat_contacts(&ctx, ChatId::new(chat_id)).await?;
|
||||
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
|
||||
}
|
||||
|
||||
/// Create a new group chat.
|
||||
///
|
||||
/// After creation,
|
||||
@@ -998,12 +993,6 @@ impl CommandApi {
|
||||
marknoticed_chat(&ctx, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Returns the message that is immediately followed by the last seen
|
||||
/// message.
|
||||
/// From the point of view of the user this is effectively
|
||||
/// "first unread", but in reality in the database a seen message
|
||||
/// _can_ be followed by a fresh (unseen) message
|
||||
/// if that message has not been individually marked as seen.
|
||||
async fn get_first_unread_message_of_chat(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1092,9 +1081,6 @@ impl CommandApi {
|
||||
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
|
||||
}
|
||||
|
||||
/// Returns all messages of a particular chat.
|
||||
/// If `add_daymarker` is `true`, it will return them as
|
||||
/// `DC_MSG_ID_DAYMARKER`, e.g. [1234, 1237, 9, 1239].
|
||||
async fn get_message_ids(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2036,6 +2022,132 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new draft (overwriting existing draft)
|
||||
///
|
||||
/// You can modify some fields of an existing draft while keeping it's message id (important to keep webxdc status updates) with the following methods:
|
||||
/// - [Self::draft_set_text]
|
||||
/// - [Self::draft_set_quoted_message]
|
||||
/// - [Self::draft_set_quoted_text]
|
||||
/// For other actions like changing the view type or file attachment you have to recreate the draft.
|
||||
///
|
||||
/// You can send the draft with [Self::send_draft]
|
||||
async fn create_draft(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut message = data
|
||||
.create_message(&ctx)
|
||||
.await
|
||||
.context("Failed to create message")?;
|
||||
|
||||
ChatId::new(chat_id)
|
||||
.set_draft(&ctx, Some(&mut message))
|
||||
.await
|
||||
.context("Failed to set draft message")?;
|
||||
Ok(message.get_id().to_u32())
|
||||
}
|
||||
|
||||
/// set text of draft
|
||||
async fn draft_set_text(&self, account_id: u32, msg_id: u32, text: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut message = message::Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
|
||||
if message.get_state() != deltachat::message::MessageState::OutDraft {
|
||||
bail!("provided message is not a draft");
|
||||
}
|
||||
|
||||
message.set_text(text);
|
||||
message
|
||||
.get_chat_id()
|
||||
.set_draft(&ctx, Some(&mut message))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// set (email) subject of draft
|
||||
async fn draft_set_subject(&self, account_id: u32, msg_id: u32, subject: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut message = message::Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
|
||||
if message.get_state() != deltachat::message::MessageState::OutDraft {
|
||||
bail!("provided message is not a draft");
|
||||
}
|
||||
|
||||
message.set_subject(subject);
|
||||
message
|
||||
.get_chat_id()
|
||||
.set_draft(&ctx, Some(&mut message))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// set quoted message id of draft
|
||||
async fn draft_set_quoted_message(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
quoted_message_id: Option<u32>,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut message = message::Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
|
||||
if message.get_state() != deltachat::message::MessageState::OutDraft {
|
||||
bail!("provided message is not a draft");
|
||||
}
|
||||
|
||||
let message_to_qoute = match quoted_message_id {
|
||||
Some(msg_id) => Some(message::Message::load_from_db(&ctx, MsgId::new(msg_id)).await?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
message.set_quote(&ctx, message_to_qoute.as_ref()).await?;
|
||||
message
|
||||
.get_chat_id()
|
||||
.set_draft(&ctx, Some(&mut message))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// set quoted text of draft
|
||||
async fn draft_set_quoted_text(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
quoted_text: Option<QuotedText>,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut message = message::Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
|
||||
if message.get_state() != deltachat::message::MessageState::OutDraft {
|
||||
bail!("provided message is not a draft");
|
||||
}
|
||||
|
||||
message.set_quote_text(quoted_text.map(|qt| (qt.text, qt.protect)));
|
||||
|
||||
message
|
||||
.get_chat_id()
|
||||
.set_draft(&ctx, Some(&mut message))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// send draft
|
||||
async fn send_draft(&self, account_id: u32, msg_id: u32) -> Result<u32> {
|
||||
// uses message id instead of chat id to force ui to have the right message id / know about the draft - reduce the chance of undefined behaviour
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut draft = message::Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
|
||||
if draft.get_state() != deltachat::message::MessageState::OutDraft {
|
||||
bail!("provided message is not a draft");
|
||||
}
|
||||
|
||||
let chat = draft.get_chat_id();
|
||||
let msg_id = chat::send_msg(&ctx, chat, &mut draft).await?.to_u32();
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
async fn send_videochat_invitation(&self, account_id: u32, chat_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
|
||||
@@ -2209,6 +2321,7 @@ impl CommandApi {
|
||||
// the better version should support:
|
||||
// - changing viewtype to enable/disable compression
|
||||
// - keeping same message id as long as attachment does not change for webxdc messages
|
||||
/// @deprecated use [Self::send_draft] instead
|
||||
async fn misc_set_draft(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -2250,6 +2363,7 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
// send the chat's current set draft
|
||||
/// @deprecated use [Self::send_draft] instead
|
||||
async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use deltachat::chat::{self, get_chat_contacts, get_past_chat_contacts, ChatVisibility};
|
||||
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
|
||||
use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::constants::Chattype;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
@@ -39,10 +39,6 @@ pub struct FullChat {
|
||||
is_self_talk: bool,
|
||||
contacts: Vec<ContactObject>,
|
||||
contact_ids: Vec<u32>,
|
||||
|
||||
/// Contact IDs of the past chat members.
|
||||
past_contact_ids: Vec<u32>,
|
||||
|
||||
color: String,
|
||||
fresh_message_counter: usize,
|
||||
// is_group - please check over chat.type in frontend instead
|
||||
@@ -63,7 +59,6 @@ impl FullChat {
|
||||
let chat = Chat::load_from_db(context, rust_chat_id).await?;
|
||||
|
||||
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
||||
let past_contact_ids = get_past_chat_contacts(context, rust_chat_id).await?;
|
||||
|
||||
let mut contacts = Vec::with_capacity(contact_ids.len());
|
||||
|
||||
@@ -116,7 +111,6 @@ impl FullChat {
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
contacts,
|
||||
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||
past_contact_ids: past_contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||
color,
|
||||
fresh_message_counter,
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
|
||||
@@ -101,7 +101,6 @@ pub enum EventType {
|
||||
/// Incoming reaction, should be notified.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingReaction {
|
||||
chat_id: u32,
|
||||
contact_id: u32,
|
||||
msg_id: u32,
|
||||
reaction: String,
|
||||
@@ -336,12 +335,10 @@ impl From<CoreEventType> for EventType {
|
||||
contact_id: contact_id.to_u32(),
|
||||
},
|
||||
CoreEventType::IncomingReaction {
|
||||
chat_id,
|
||||
contact_id,
|
||||
msg_id,
|
||||
reaction,
|
||||
} => IncomingReaction {
|
||||
chat_id: chat_id.to_u32(),
|
||||
contact_id: contact_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
reaction: reaction.as_str().to_string(),
|
||||
|
||||
@@ -104,9 +104,6 @@ enum MessageQuote {
|
||||
WithMessage {
|
||||
text: String,
|
||||
message_id: u32,
|
||||
/// The quoted message does not always belong
|
||||
/// to the same chat, e.g. when "Reply Privately" is used.
|
||||
chat_id: u32,
|
||||
author_display_name: String,
|
||||
author_display_color: String,
|
||||
override_sender_name: Option<String>,
|
||||
@@ -150,7 +147,6 @@ impl MessageObject {
|
||||
Some(MessageQuote::WithMessage {
|
||||
text: quoted_text,
|
||||
message_id: quote.get_id().to_u32(),
|
||||
chat_id: quote.get_chat_id().to_u32(),
|
||||
author_display_name: quote_author.get_display_name().to_owned(),
|
||||
author_display_color: color_int_to_hex_string(quote_author.get_color()),
|
||||
override_sender_name: quote.get_override_sender_name(),
|
||||
@@ -712,3 +708,13 @@ impl From<deltachat::ephemeral::Timer> for EphemeralTimer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QuotedText {
|
||||
/// Text shown in the Quote
|
||||
pub text: String,
|
||||
/// protect specifies whether text should only be sent encrypted.
|
||||
/// If it should, but the message is unencrypted, text is replaced with "...".
|
||||
pub protect: bool,
|
||||
}
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.155.4"
|
||||
"version": "1.153.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.155.4"
|
||||
version = "1.153.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
@@ -8,12 +8,12 @@ repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
deltachat = { workspace = true, features = ["internals"]}
|
||||
dirs = "6"
|
||||
dirs = "5"
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
qr2term = "0.3.3"
|
||||
rusqlite = { workspace = true }
|
||||
rustyline = "15"
|
||||
rustyline = "14"
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
|
||||
@@ -939,7 +939,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
} else {
|
||||
Viewtype::File
|
||||
});
|
||||
msg.set_file_and_deduplicate(&context, Path::new(arg1), None, None)?;
|
||||
msg.set_file(arg1, None);
|
||||
msg.set_text(arg2.to_string());
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use log::{error, info, warn};
|
||||
use nu_ansi_term::Color;
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::highlight::{CmdKind as HighlightCmdKind, Highlighter, MatchingBracketHighlighter};
|
||||
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
||||
use rustyline::hint::{Hinter, HistoryHinter};
|
||||
use rustyline::validate::Validator;
|
||||
use rustyline::{
|
||||
@@ -298,8 +298,8 @@ impl Highlighter for DcHelper {
|
||||
self.highlighter.highlight(line, pos)
|
||||
}
|
||||
|
||||
fn highlight_char(&self, line: &str, pos: usize, kind: HighlightCmdKind) -> bool {
|
||||
self.highlighter.highlight_char(line, pos, kind)
|
||||
fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
|
||||
self.highlighter.highlight_char(line, pos, forced)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.155.4"
|
||||
version = "1.153.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -238,11 +238,6 @@ class Chat:
|
||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||
|
||||
def get_past_contacts(self) -> list[Contact]:
|
||||
"""Get past contacts for this chat."""
|
||||
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in past_contacts]
|
||||
|
||||
def set_image(self, path: str) -> None:
|
||||
"""Set profile image of this chat.
|
||||
|
||||
|
||||
@@ -231,9 +231,7 @@ def test_chat(acfactory) -> None:
|
||||
group.get_fresh_message_count()
|
||||
group.mark_noticed()
|
||||
assert group.get_contacts()
|
||||
assert group.get_past_contacts() == []
|
||||
group.remove_contact(alice_contact_bob)
|
||||
assert len(group.get_past_contacts()) == 1
|
||||
group.remove_contact(alice_chat_bob)
|
||||
group.get_locations()
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.155.4"
|
||||
version = "1.153.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.155.4"
|
||||
"version": "1.153.0"
|
||||
}
|
||||
|
||||
51
deny.toml
51
deny.toml
@@ -12,8 +12,15 @@ ignore = [
|
||||
# Unmaintained encoding
|
||||
"RUSTSEC-2021-0153",
|
||||
|
||||
# Unmaintained proc-macro-error
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2024-0370>
|
||||
"RUSTSEC-2024-0370",
|
||||
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
|
||||
# idna 0.5.0
|
||||
"RUSTSEC-2024-0421",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -26,15 +33,15 @@ skip = [
|
||||
{ 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" },
|
||||
{ name = "generator", version = "0.7.5" },
|
||||
{ name = "event-listener", version = "4.0.3" },
|
||||
{ name = "fastrand", version = "1.9.0" },
|
||||
{ name = "fiat-crypto", version = "0.1.20" },
|
||||
{ name = "futures-lite", version = "1.13.0" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "loom", version = "0.5.6" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "idna", version = "0.5.0" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "nix", version = "0.27.1" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
@@ -42,33 +49,24 @@ skip = [
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rtnetlink", version = "0.13.1" },
|
||||
{ name = "security-framework", version = "2.11.1" },
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "tokio-tungstenite", version = "0.21.0" },
|
||||
{ name = "tungstenite", version = "0.21.0" },
|
||||
{ name = "unicode-width", version = "0.1.11" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "windows" },
|
||||
{ name = "windows_aarch64_gnullvm" },
|
||||
{ name = "windows_aarch64_msvc" },
|
||||
{ name = "windows-core" },
|
||||
{ name = "windows_i686_gnu" },
|
||||
{ name = "windows_i686_gnullvm" },
|
||||
{ name = "windows_i686_msvc" },
|
||||
{ name = "windows-implement" },
|
||||
{ name = "windows-interface" },
|
||||
{ name = "windows-result" },
|
||||
{ name = "windows-strings" },
|
||||
{ name = "windows-sys" },
|
||||
{ name = "windows-targets" },
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_aarch64_msvc", version = "<0.52" },
|
||||
{ name = "windows-core", version = "<0.54.0" },
|
||||
{ name = "windows_i686_gnu", version = "<0.52" },
|
||||
{ name = "windows_i686_msvc", version = "<0.52" },
|
||||
{ name = "windows-sys", version = "<0.59" },
|
||||
{ name = "windows-targets", version = "<0.52" },
|
||||
{ name = "windows", version = "<0.54.0" },
|
||||
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_x86_64_gnu", version = "<0.52" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.52" },
|
||||
{ name = "winreg", version = "0.50.0" },
|
||||
]
|
||||
|
||||
|
||||
@@ -99,5 +97,6 @@ license-files = [
|
||||
[sources.allow-org]
|
||||
# Organisations which we allow git sources from.
|
||||
github = [
|
||||
"async-email",
|
||||
"deltachat",
|
||||
]
|
||||
|
||||
74
flake.lock
generated
74
flake.lock
generated
@@ -47,16 +47,17 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1737527504,
|
||||
"narHash": "sha256-Z8S5gLPdIYeKwBXDaSxlJ72ZmiilYhu3418h3RSQZA0=",
|
||||
"lastModified": 1711088506,
|
||||
"narHash": "sha256-USdlY7Tx2oJWqFBpp10+03+h7eVhpkQ4s9t1ERjeIJE=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "aa13f23e3e91b95377a693ac655bbc6545ebec0d",
|
||||
"rev": "85f4139f3c092cf4afd9f9906d7ed218ef262c97",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "85f4139f3c092cf4afd9f9906d7ed218ef262c97",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
@@ -114,6 +115,25 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"new-fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_4",
|
||||
"rust-analyzer-src": "rust-analyzer-src_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1734417396,
|
||||
"narHash": "sha256-32x1Z+Pz3Jv0cK9EG56cFTKXy/mZ/c+Ikxw+aVfKHp4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "a18d41b26e998e95a598858fdb86ba22fb5da47d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1730207686,
|
||||
@@ -147,11 +167,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1737469691,
|
||||
"narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
|
||||
"lastModified": 1731139594,
|
||||
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
|
||||
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -174,6 +194,22 @@
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1734119587,
|
||||
"narHash": "sha256-AKU6qqskl0yf2+JdRdD0cfxX4b9x3KKV5RqA6wijmPM=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3566ab7246670a43abd2ffa913cc62dad9cdf7d5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_5": {
|
||||
"locked": {
|
||||
"lastModified": 1731139594,
|
||||
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||
@@ -195,18 +231,36 @@
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"naersk": "naersk",
|
||||
"new-fenix": "new-fenix",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
"nixpkgs": "nixpkgs_5"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1737453499,
|
||||
"narHash": "sha256-fa5AJI9mjFU2oVXqdCq2oA2pripAXbHzkUkewJRQpxA=",
|
||||
"lastModified": 1731342671,
|
||||
"narHash": "sha256-36eYDHoPzjavnpmEpc2MXdzMk557S0YooGms07mDuKk=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "0b68402d781955d526b80e5d479e9e47addb4075",
|
||||
"rev": "fc98e0657abf3ce07eed513e38274c89bbb2f8ad",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1734386068,
|
||||
"narHash": "sha256-Py025JiD9lcPmldB7X1AEjq3WBTS60jZUJRtTDonmaE=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "0a706f7d2ac093985eae317781200689cfd48b78",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
16
flake.nix
16
flake.nix
@@ -1,14 +1,19 @@
|
||||
{
|
||||
description = "Delta Chat core";
|
||||
inputs = {
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
# Old Rust to build releases.
|
||||
fenix.url = "github:nix-community/fenix?rev=85f4139f3c092cf4afd9f9906d7ed218ef262c97";
|
||||
|
||||
# New Rust for development shell.
|
||||
new-fenix.url = "github:nix-community/fenix";
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
android.url = "github:tadfisher/android-nixpkgs";
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
|
||||
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, new-fenix, android }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
@@ -88,7 +93,8 @@
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
"email-0.0.20" = "sha256-cfR3D5jFQpw32bGsgapK2Uwuxmht+rRK/n1ZUmCb2WA=";
|
||||
"email-0.0.20" = "sha256-rV4Uzqt2Qdrfi5Ti1r+Si1c2iW1kKyWLwOgLkQ5JGGw=";
|
||||
"encoded-words-0.2.0" = "sha256-KK9st0hLFh4dsrnLd6D8lC6pRFFs8W+WpZSGMGJcosk=";
|
||||
"lettre-0.9.2" = "sha256-+hU1cFacyyeC9UGVBpS14BWlJjHy90i/3ynMkKAzclk=";
|
||||
};
|
||||
};
|
||||
@@ -538,13 +544,13 @@
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
system = system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
overlays = [ new-fenix.overlays.default ];
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
(new-fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
|
||||
@@ -8,7 +8,7 @@ import { EventId2EventName, C } from '../dist/constants.js'
|
||||
import { join } from 'path'
|
||||
import { statSync } from 'fs'
|
||||
import { Context } from '../dist/context.js'
|
||||
import { fileURLToPath } from 'url';
|
||||
import {fileURLToPath} from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
@@ -444,7 +444,7 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
context.setChatProfileImage(chatId, imagePath)
|
||||
const blobPath = context.getChat(chatId).getProfileImage()
|
||||
expect(blobPath.startsWith(blobs)).to.be.true
|
||||
expect(blobPath.includes('image')).to.be.false
|
||||
expect(blobPath.includes('image')).to.be.true
|
||||
expect(blobPath.endsWith('.jpeg')).to.be.true
|
||||
|
||||
context.setChatProfileImage(chatId, null)
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.155.4"
|
||||
"version": "1.153.0"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.155.4"
|
||||
version = "1.153.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -108,9 +108,7 @@ class Message:
|
||||
|
||||
@props.with_doc
|
||||
def filename(self):
|
||||
"""file path if there was an attachment, otherwise empty string.
|
||||
If you want to get the file extension or a user-visible string,
|
||||
use `basename` instead."""
|
||||
"""filename if there was an attachment, otherwise empty string."""
|
||||
return from_dc_charpointer(lib.dc_msg_get_file(self._dc_msg))
|
||||
|
||||
def set_file(self, path, mime_type=None):
|
||||
@@ -122,8 +120,7 @@ class Message:
|
||||
|
||||
@props.with_doc
|
||||
def basename(self) -> str:
|
||||
"""The user-visible name of the attachment (incl. extension)
|
||||
if it exists, otherwise empty string."""
|
||||
"""basename of the attachment if it exists, otherwise empty string."""
|
||||
# FIXME, it does not return basename
|
||||
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
||||
|
||||
|
||||
@@ -181,16 +181,15 @@ def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
|
||||
msg = send_and_receive_message()
|
||||
assert msg.text == "withfile"
|
||||
assert open(msg.filename).read() == "some data"
|
||||
msg.basename.index(basename)
|
||||
assert msg.basename.endswith(ext)
|
||||
msg.filename.index(basename)
|
||||
assert msg.filename.endswith(ext)
|
||||
|
||||
msg2 = send_and_receive_message()
|
||||
assert msg2.text == "withfile"
|
||||
assert open(msg2.filename).read() == "some data"
|
||||
msg2.basename.index(basename)
|
||||
assert msg2.basename.endswith(ext)
|
||||
assert msg.filename == msg2.filename # The file is deduplicated
|
||||
assert msg.basename == msg2.basename
|
||||
msg2.filename.index(basename)
|
||||
assert msg2.filename.endswith(ext)
|
||||
assert msg.filename != msg2.filename
|
||||
|
||||
|
||||
def test_send_file_html_attachment(tmp_path, acfactory, lp):
|
||||
@@ -215,8 +214,8 @@ def test_send_file_html_attachment(tmp_path, acfactory, lp):
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
|
||||
assert open(msg.filename).read() == content
|
||||
msg.basename.index(basename)
|
||||
assert msg.basename.endswith(ext)
|
||||
msg.filename.index(basename)
|
||||
assert msg.filename.endswith(ext)
|
||||
|
||||
|
||||
def test_html_message(acfactory, lp):
|
||||
@@ -1983,7 +1982,7 @@ def test_fetch_deleted_msg(acfactory, lp):
|
||||
if ev.name == "DC_EVENT_MSGS_CHANGED":
|
||||
pytest.fail("A deleted message was shown to the user")
|
||||
|
||||
if ev.name == "DC_EVENT_INFO" and 'IDLE entering wait-on-remote state in folder "INBOX".' in ev.data2:
|
||||
if ev.name == "DC_EVENT_INFO" and "INBOX: Idle entering wait-on-remote state" in ev.data2:
|
||||
break # DC is done with reading messages
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-02-10
|
||||
2025-01-05
|
||||
@@ -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.84.1
|
||||
RUST_VERSION=1.84.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
974
src/blob.rs
974
src/blob.rs
File diff suppressed because it is too large
Load Diff
@@ -1,810 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::sql;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
||||
tokio::task::block_in_place(move || {
|
||||
let img = ImageReader::open(path)
|
||||
.expect("failed to open image")
|
||||
.with_guessed_format()
|
||||
.expect("failed to guess format")
|
||||
.decode()
|
||||
.expect("failed to decode image");
|
||||
assert_eq!(img.width(), width, "invalid width");
|
||||
assert_eq!(img.height(), height, "invalid height");
|
||||
img
|
||||
})
|
||||
}
|
||||
|
||||
const FILE_BYTES: &[u8] = b"hello";
|
||||
const FILE_DEDUPLICATED: &str = "ea8f163db38682925e4491c5e58d4bb.txt";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||
let fname = t.get_blobdir().join(FILE_DEDUPLICATED);
|
||||
let data = fs::read(fname).await.unwrap();
|
||||
assert_eq!(data, FILE_BYTES);
|
||||
assert_eq!(blob.as_name(), format!("$BLOBDIR/{FILE_DEDUPLICATED}"));
|
||||
assert_eq!(blob.to_abs_path(), t.get_blobdir().join(FILE_DEDUPLICATED));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lowercase_ext() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.TXT").unwrap();
|
||||
assert!(
|
||||
blob.as_name().ends_with(".txt"),
|
||||
"Blob {blob:?} should end with .txt"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_as_file_name() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||
assert_eq!(blob.as_file_name(), FILE_DEDUPLICATED);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_as_rel_path() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||
assert_eq!(blob.as_rel_path(), Path::new(FILE_DEDUPLICATED));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_suffix() {
|
||||
let t = TestContext::new().await;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||
assert_eq!(blob.suffix(), Some("txt"));
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "bar").unwrap();
|
||||
assert_eq!(blob.suffix(), None);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_dup() {
|
||||
let t = TestContext::new().await;
|
||||
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
||||
let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED);
|
||||
assert!(foo_path.exists());
|
||||
BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.txt").unwrap();
|
||||
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
|
||||
while let Ok(Some(dirent)) = dir.next_entry().await {
|
||||
let fname = dirent.file_name();
|
||||
if fname == foo_path.file_name().unwrap() {
|
||||
assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
|
||||
} else {
|
||||
let name = fname.to_str().unwrap();
|
||||
assert!(name.ends_with(".txt"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_double_ext() {
|
||||
let t = TestContext::new().await;
|
||||
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.tar.gz").unwrap();
|
||||
let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED).with_extension("gz");
|
||||
assert!(foo_path.exists());
|
||||
BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.tar.gz").unwrap();
|
||||
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
|
||||
while let Ok(Some(dirent)) = dir.next_entry().await {
|
||||
let fname = dirent.file_name();
|
||||
if fname == foo_path.file_name().unwrap() {
|
||||
assert_eq!(fs::read(&foo_path).await.unwrap(), FILE_BYTES);
|
||||
} else {
|
||||
let name = fname.to_str().unwrap();
|
||||
println!("{name}");
|
||||
assert_eq!(name.starts_with("foo"), false);
|
||||
assert_eq!(name.ends_with(".tar.gz"), false);
|
||||
assert!(name.ends_with(".gz"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_long_names() {
|
||||
let t = TestContext::new().await;
|
||||
let s = format!("file.{}", "a".repeat(100));
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap();
|
||||
let blobname = blob.as_name().split('/').last().unwrap();
|
||||
assert!(blobname.len() < 70);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_and_copy() {
|
||||
let t = TestContext::new().await;
|
||||
let src = t.dir.path().join("src");
|
||||
fs::write(&src, b"boo").await.unwrap();
|
||||
let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/src");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
|
||||
let whoops = t.dir.path().join("whoops");
|
||||
assert!(BlobObject::create_and_copy(&t, whoops.as_ref())
|
||||
.await
|
||||
.is_err());
|
||||
let whoops = t.get_blobdir().join("whoops");
|
||||
assert!(!whoops.exists());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_from_path() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let src_ext = t.dir.path().join("external");
|
||||
fs::write(&src_ext, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/external");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
|
||||
let src_int = t.get_blobdir().join("internal");
|
||||
fs::write(&src_int, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t, &src_int).await.unwrap();
|
||||
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
|
||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||
assert_eq!(data, b"boo");
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_from_name_long() {
|
||||
let t = TestContext::new().await;
|
||||
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
||||
fs::write(&src_ext, b"boo").await.unwrap();
|
||||
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
blob.as_name(),
|
||||
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_blob_name() {
|
||||
assert!(BlobObject::is_acceptible_blob_name("foo"));
|
||||
assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
|
||||
assert!(BlobObject::is_acceptible_blob_name("f".repeat(128)));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitise_name() {
|
||||
let (stem, ext) = BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
|
||||
assert_eq!(ext, ".txt");
|
||||
assert!(!stem.is_empty());
|
||||
|
||||
// the extensions are kept together as between stem and extension a number may be added -
|
||||
// and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz`
|
||||
let (stem, ext) = BlobObject::sanitise_name("wot.tar.gz");
|
||||
assert_eq!(stem, "wot");
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name(".foo.bar");
|
||||
assert_eq!(stem, "");
|
||||
assert_eq!(ext, ".foo.bar");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
|
||||
assert!(stem.contains("foo"));
|
||||
assert!(!stem.contains('?'));
|
||||
assert_eq!(ext, ".bar");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("no-extension");
|
||||
assert_eq!(stem, "no-extension");
|
||||
assert_eq!(ext, "");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("path/ignored\\this: is* forbidden?.c");
|
||||
assert_eq!(ext, ".c");
|
||||
assert!(!stem.contains("path"));
|
||||
assert!(!stem.contains("ignored"));
|
||||
assert!(stem.contains("this"));
|
||||
assert!(stem.contains("forbidden"));
|
||||
assert!(!stem.contains('/'));
|
||||
assert!(!stem.contains('\\'));
|
||||
assert!(!stem.contains(':'));
|
||||
assert!(!stem.contains('*'));
|
||||
assert!(!stem.contains('?'));
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name(
|
||||
"file.with_lots_of_characters_behind_point_and_double_ending.tar.gz",
|
||||
);
|
||||
assert_eq!(
|
||||
stem,
|
||||
"file.with_lots_of_characters_behind_point_and_double_ending"
|
||||
);
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("a. tar.tar.gz");
|
||||
assert_eq!(stem, "a. tar");
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("Guia_uso_GNB (v0.8).pdf");
|
||||
assert_eq!(stem, "Guia_uso_GNB (v0.8)");
|
||||
assert_eq!(ext, ".pdf");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_white_bg() {
|
||||
let t = TestContext::new().await;
|
||||
let bytes0 = include_bytes!("../../test-data/image/logo.png").as_slice();
|
||||
let bytes1 = include_bytes!("../../test-data/image/avatar900x900.png").as_slice();
|
||||
for (bytes, color) in [
|
||||
(bytes0, [255u8, 255, 255, 255]),
|
||||
(bytes1, [253u8, 198, 0, 255]),
|
||||
] {
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
fs::write(&avatar_src, bytes).await.unwrap();
|
||||
|
||||
let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap();
|
||||
let img_wh = 128;
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits)
|
||||
.unwrap();
|
||||
tokio::task::block_in_place(move || {
|
||||
let img = ImageReader::open(blob.to_abs_path())
|
||||
.unwrap()
|
||||
.with_guessed_format()
|
||||
.unwrap()
|
||||
.decode()
|
||||
.unwrap();
|
||||
assert!(img.width() == img_wh);
|
||||
assert!(img.height() == img_wh);
|
||||
assert_eq!(img.get_pixel(0, 0), Rgba(color));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_outside_blobdir() {
|
||||
async fn file_size(path_buf: &Path) -> u64 {
|
||||
fs::metadata(path_buf).await.unwrap().len()
|
||||
}
|
||||
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.dir.path().join("avatar.jpg");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
|
||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
let avatar_path = Path::new(&avatar_blob);
|
||||
assert!(
|
||||
avatar_blob.ends_with("d98cd30ed8f2129bf3968420208849d.jpg"),
|
||||
"The avatar filename should be its hash, put instead it's {avatar_blob}"
|
||||
);
|
||||
let scaled_avatar_size = file_size(avatar_path).await;
|
||||
assert!(scaled_avatar_size < avatar_bytes.len() as u64);
|
||||
|
||||
check_image_size(avatar_src, 1000, 1000);
|
||||
check_image_size(
|
||||
&avatar_blob,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
);
|
||||
|
||||
let mut blob = BlobObject::new_from_path(&t, avatar_path).await.unwrap();
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
|
||||
.unwrap();
|
||||
let new_file_size = file_size(&blob.to_abs_path()).await;
|
||||
assert!(new_file_size <= 3000);
|
||||
assert!(new_file_size > 2000);
|
||||
// The new file should be smaller:
|
||||
assert!(new_file_size < scaled_avatar_size);
|
||||
// And the original file should not be touched:
|
||||
assert_eq!(file_size(avatar_path).await, scaled_avatar_size);
|
||||
tokio::task::block_in_place(move || {
|
||||
let img = ImageReader::open(blob.to_abs_path())
|
||||
.unwrap()
|
||||
.with_guessed_format()
|
||||
.unwrap()
|
||||
.decode()
|
||||
.unwrap();
|
||||
assert!(img.width() > 130);
|
||||
assert_eq!(img.width(), img.height());
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_in_blobdir() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.get_blobdir().join("avatar.png");
|
||||
fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
check_image_size(&avatar_src, 900, 900);
|
||||
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert!(
|
||||
avatar_cfg.ends_with("9e7f409ac5c92b942cc4f31cee2770a.png"),
|
||||
"Avatar file name {avatar_cfg} should end with its hash"
|
||||
);
|
||||
|
||||
check_image_size(
|
||||
avatar_cfg,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_copy_without_recode() {
|
||||
let t = TestContext::new().await;
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||
let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png");
|
||||
assert!(!avatar_blob.exists());
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(avatar_blob.exists());
|
||||
assert_eq!(
|
||||
fs::metadata(&avatar_blob).await.unwrap().len(),
|
||||
avatar_bytes.len() as u64
|
||||
);
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_1() {
|
||||
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1000,
|
||||
original_height: 1000,
|
||||
compressed_width: 1000,
|
||||
compressed_height: 1000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1000,
|
||||
original_height: 1000,
|
||||
compressed_width: 1000,
|
||||
compressed_height: 1000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_2() {
|
||||
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
||||
let bytes = include_bytes!("../../test-data/image/rectangle2000x1800-rotated.jpg");
|
||||
let img_rotated = SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 2000,
|
||||
original_height: 1800,
|
||||
orientation: 270,
|
||||
compressed_width: 1800,
|
||||
compressed_height: 2000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
|
||||
let mut buf = Cursor::new(vec![]);
|
||||
img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
|
||||
let bytes = buf.into_inner();
|
||||
|
||||
let img_rotated = SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes: &bytes,
|
||||
extension: "jpg",
|
||||
original_width: 1800,
|
||||
original_height: 2000,
|
||||
compressed_width: 1800,
|
||||
compressed_height: 2000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_balanced_png() {
|
||||
let bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::File,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::File,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
set_draft: true,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Sticker,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Tests that RGBA PNG can be recoded into JPEG
|
||||
/// by dropping alpha channel.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_rgba_png_to_jpeg() {
|
||||
let bytes = include_bytes!("../../test-data/image/screenshot-rgba.png");
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_huge_jpg() {
|
||||
let bytes = include_bytes!("../../test-data/image/screenshot.jpg");
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::BALANCED_IMAGE_SIZE,
|
||||
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn assert_correct_rotation(img: &DynamicImage) {
|
||||
// The test images are black in the bottom left corner after correctly applying
|
||||
// the EXIF orientation
|
||||
|
||||
let [luma] = img.get_pixel(10, 10).to_luma().0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img
|
||||
.get_pixel(img.width() - 10, img.height() - 10)
|
||||
.to_luma()
|
||||
.0;
|
||||
assert_eq!(luma, 255);
|
||||
let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
|
||||
assert_eq!(luma, 0);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SendImageCheckMediaquality<'a> {
|
||||
pub(crate) viewtype: Viewtype,
|
||||
pub(crate) media_quality_config: &'a str,
|
||||
pub(crate) bytes: &'a [u8],
|
||||
pub(crate) extension: &'a str,
|
||||
pub(crate) has_exif: bool,
|
||||
pub(crate) original_width: u32,
|
||||
pub(crate) original_height: u32,
|
||||
pub(crate) orientation: i32,
|
||||
pub(crate) compressed_width: u32,
|
||||
pub(crate) compressed_height: u32,
|
||||
pub(crate) set_draft: bool,
|
||||
}
|
||||
|
||||
impl SendImageCheckMediaquality<'_> {
|
||||
pub(crate) async fn test(self) -> anyhow::Result<DynamicImage> {
|
||||
let viewtype = self.viewtype;
|
||||
let media_quality_config = self.media_quality_config;
|
||||
let bytes = self.bytes;
|
||||
let extension = self.extension;
|
||||
let has_exif = self.has_exif;
|
||||
let original_width = self.original_width;
|
||||
let original_height = self.original_height;
|
||||
let orientation = self.orientation;
|
||||
let compressed_width = self.compressed_width;
|
||||
let compressed_height = self.compressed_height;
|
||||
let set_draft = self.set_draft;
|
||||
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(Config::MediaQuality, Some(media_quality_config))
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file").with_extension(extension);
|
||||
let file_name = format!("file.{extension}");
|
||||
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
.context("failed to write file")?;
|
||||
check_image_size(&file, original_width, original_height);
|
||||
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
|
||||
if has_exif {
|
||||
let exif = exif.unwrap();
|
||||
assert_eq!(exif_orientation(&exif, &alice), orientation);
|
||||
} else {
|
||||
assert!(exif.is_none());
|
||||
}
|
||||
|
||||
let mut msg = Message::new(viewtype);
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some(&file_name), None)?;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
if set_draft {
|
||||
chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap();
|
||||
msg = chat.id.get_draft(&alice).await.unwrap().unwrap();
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
||||
}
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = alice
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
|
||||
alice_msg.save_file(&alice, &file_saved).await?;
|
||||
check_image_size(file_saved, compressed_width, compressed_height);
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = bob
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
if viewtype == Viewtype::File {
|
||||
assert_eq!(file_saved.extension().unwrap(), extension);
|
||||
let bytes1 = fs::read(&file_saved).await?;
|
||||
assert_eq!(&bytes1, bytes);
|
||||
}
|
||||
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert!(exif.is_none());
|
||||
|
||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_big_gif_as_image() -> Result<()> {
|
||||
let bytes = include_bytes!("../../test-data/image/screenshot.gif");
|
||||
let (width, height) = (1920u32, 1080u32);
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(
|
||||
Config::MediaQuality,
|
||||
Some(&(MediaQuality::Worse as i32).to_string()),
|
||||
)
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file").with_extension("gif");
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
.context("failed to write file")?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some("file.gif"), None)?;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
// DC must detect the image as GIF and send it w/o reencoding.
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif);
|
||||
assert_eq!(bob_msg.get_width() as u32, width);
|
||||
assert_eq!(bob_msg.get_height() as u32, height);
|
||||
let file_saved = bob
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert_eq!(file_size, bytes.len() as u64);
|
||||
check_image_size(file_saved, width, height);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_gif_as_sticker() -> Result<()> {
|
||||
let bytes = include_bytes!("../../test-data/image/image100x50.gif");
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let file = alice.get_blobdir().join("file").with_extension("gif");
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
.context("failed to write file")?;
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(alice, &file, None, None)?;
|
||||
let chat = alice.get_self_chat().await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
|
||||
// Message::force_sticker() wasn't used, still Viewtype::Sticker is preserved because of the
|
||||
// extension.
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_and_deduplicate() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let path = t.get_blobdir().join("anyfile.dat");
|
||||
fs::write(&path, b"bla").await?;
|
||||
let blob = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
||||
assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f.dat");
|
||||
assert_eq!(path.exists(), false);
|
||||
|
||||
assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
|
||||
|
||||
fs::write(&path, b"bla").await?;
|
||||
let blob2 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
||||
assert_eq!(blob2.name, blob.name);
|
||||
|
||||
let path_outside_blobdir = t.dir.path().join("anyfile.dat");
|
||||
fs::write(&path_outside_blobdir, b"bla").await?;
|
||||
let blob3 =
|
||||
BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
|
||||
assert!(path_outside_blobdir.exists());
|
||||
assert_eq!(blob3.name, blob.name);
|
||||
|
||||
fs::write(&path, b"blabla").await?;
|
||||
let blob4 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
||||
assert_ne!(blob4.name, blob.name);
|
||||
|
||||
fs::remove_dir_all(t.get_blobdir()).await?;
|
||||
let blob5 =
|
||||
BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
|
||||
assert_eq!(blob5.name, blob.name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_and_deduplicate_from_bytes() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
fs::remove_dir(t.get_blobdir()).await?;
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
|
||||
assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f");
|
||||
|
||||
assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
|
||||
let modified1 = blob.to_abs_path().metadata()?.modified()?;
|
||||
|
||||
// Test that the modification time of the file is updated when a new file is created
|
||||
// so that it's not deleted during housekeeping.
|
||||
// We can't use SystemTime::shift() here because file creation uses the actual OS time,
|
||||
// which we can't mock from our code.
|
||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||
|
||||
let blob2 = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
|
||||
assert_eq!(blob2.name, blob.name);
|
||||
|
||||
let modified2 = blob.to_abs_path().metadata()?.modified()?;
|
||||
assert_ne!(modified1, modified2);
|
||||
sql::housekeeping(&t).await?;
|
||||
assert!(blob2.to_abs_path().exists());
|
||||
|
||||
// If we do shift the time by more than 1h, the blob file will be deleted during housekeeping:
|
||||
SystemTime::shift(Duration::from_secs(65 * 60));
|
||||
sql::housekeeping(&t).await?;
|
||||
assert_eq!(blob2.to_abs_path().exists(), false);
|
||||
|
||||
let blob3 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
|
||||
assert_ne!(blob3.name, blob.name);
|
||||
|
||||
{
|
||||
// If something goes wrong and the blob file is overwritten,
|
||||
// the correct content should be restored:
|
||||
fs::write(blob3.to_abs_path(), b"bloblo").await?;
|
||||
|
||||
let blob4 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
|
||||
let blob4_content = fs::read(blob4.to_abs_path()).await?;
|
||||
assert_eq!(blob4_content, b"blabla");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
3496
src/chat.rs
3496
src/chat.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -143,10 +143,7 @@ pub enum Config {
|
||||
/// Send BCC copy to self.
|
||||
///
|
||||
/// Should be enabled for multidevice setups.
|
||||
/// Default is 0 for chatmail accounts, 1 otherwise.
|
||||
///
|
||||
/// This is automatically enabled when importing/exporting a backup,
|
||||
/// setting up a second device, or receiving a sync message.
|
||||
/// Default is 0 for chatmail accounts before a backup export, 1 otherwise.
|
||||
BccSelf,
|
||||
|
||||
/// True if encryption is preferred according to Autocrypt standard.
|
||||
@@ -387,11 +384,6 @@ pub enum Config {
|
||||
|
||||
/// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set
|
||||
/// and `Bot` unset.
|
||||
///
|
||||
/// On real devices, this is usually always enabled and `BccSelf` is the only setting
|
||||
/// that controls whether sync messages are sent.
|
||||
///
|
||||
/// In tests, this is usually disabled.
|
||||
#[strum(props(default = "1"))]
|
||||
SyncMsgs,
|
||||
|
||||
@@ -455,13 +447,6 @@ pub enum Config {
|
||||
/// If it has not changed, we do not store
|
||||
/// the device token again.
|
||||
DeviceToken,
|
||||
|
||||
/// Device token encrypted with OpenPGP.
|
||||
///
|
||||
/// We store encrypted token next to `device_token`
|
||||
/// to avoid encrypting it differently and
|
||||
/// storing the same token multiple times on the server.
|
||||
EncryptedDeviceToken,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -693,7 +678,7 @@ impl Context {
|
||||
let value = match key {
|
||||
Config::Selfavatar if value.is_empty() => None,
|
||||
Config::Selfavatar => {
|
||||
config_value = BlobObject::store_from_base64(self, value)?;
|
||||
config_value = BlobObject::store_from_base64(self, value, "avatar").await?;
|
||||
Some(config_value.as_str())
|
||||
}
|
||||
_ => Some(value),
|
||||
@@ -771,8 +756,7 @@ impl Context {
|
||||
.await?;
|
||||
match value {
|
||||
Some(path) => {
|
||||
let path = get_abs_path(self, Path::new(path));
|
||||
let mut blob = BlobObject::create_and_deduplicate(self, &path, &path)?;
|
||||
let mut blob = BlobObject::new_from_path(self, path.as_ref()).await?;
|
||||
blob.recode_to_avatar_size(self).await?;
|
||||
self.sql
|
||||
.set_raw_config(key.as_ref(), Some(blob.as_name()))
|
||||
@@ -1151,8 +1135,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync() -> Result<()> {
|
||||
let alice0 = TestContext::new_alice().await;
|
||||
@@ -1227,7 +1209,7 @@ mod tests {
|
||||
let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap();
|
||||
assert_eq!(
|
||||
self_chat_avatar_path,
|
||||
alice0.get_blobdir().join(SAVED_MESSAGES_DEDUPLICATED_FILE)
|
||||
alice0.get_blobdir().join("icon-saved-messages.png")
|
||||
);
|
||||
assert!(alice1
|
||||
.get_config(Config::Selfavatar)
|
||||
|
||||
1349
src/contact.rs
1349
src/contact.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
651
src/context.rs
651
src/context.rs
@@ -1480,4 +1480,653 @@ pub fn get_version_str() -> &'static str {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod context_tests;
|
||||
mod tests {
|
||||
use anyhow::Context as _;
|
||||
use strum::IntoEnumIterator;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, MuteDuration};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
use crate::tools::{create_outgoing_rfc724_mid, SystemTime};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_db() -> Result<()> {
|
||||
let tmp = tempfile::tempdir()?;
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
tokio::fs::write(&dbfile, b"123").await?;
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?;
|
||||
|
||||
// Broken database is indistinguishable from encrypted one.
|
||||
assert_eq!(res.is_open().await, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs() {
|
||||
let t = TestContext::new().await;
|
||||
let fresh = t.get_fresh_msgs().await.unwrap();
|
||||
assert!(fresh.is_empty())
|
||||
}
|
||||
|
||||
async fn receive_msg(t: &TestContext, chat: &Chat) {
|
||||
let members = get_chat_contacts(t, chat.id).await.unwrap();
|
||||
let contact = Contact::get_by_id(t, *members.first().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = format!(
|
||||
"From: {}\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <{}>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
contact.get_addr(),
|
||||
create_outgoing_rfc724_mid()
|
||||
);
|
||||
println!("{msg}");
|
||||
receive_imf(t, msg.as_bytes(), false).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs_and_muted_chats() {
|
||||
// receive various mails in 3 chats
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
let claire = t.create_chat_with_contact("", "claire@g.it").await;
|
||||
let dave = t.create_chat_with_contact("", "dave@g.it").await;
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
||||
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 2);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
|
||||
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, dave.id).await.unwrap().len(), 3);
|
||||
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
|
||||
|
||||
// mute one of the chats
|
||||
set_muted(&t, claire.id, MuteDuration::Forever)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
|
||||
|
||||
// receive more messages
|
||||
receive_msg(&t, &bob).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 3);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
|
||||
|
||||
// unmute claire again
|
||||
set_muted(&t, claire.id, MuteDuration::NotMuted)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs_and_muted_until() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
||||
|
||||
// chat is unmuted by default, here and in the following assert(),
|
||||
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
|
||||
// have the same view to the database.
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
// test get_fresh_msgs() with mute_until in the future
|
||||
set_muted(
|
||||
&t,
|
||||
bob.id,
|
||||
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
// to test get_fresh_msgs() with mute_until in the past,
|
||||
// we need to modify the database directly
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
(time() - 3600, bob.id),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
// test get_fresh_msgs() with "forever" mute_until
|
||||
set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
||||
// that results in "muted forever" by definition.
|
||||
t.sql
|
||||
.execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_muted_context() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
t.set_config(Config::IsMuted, Some("1")).await?;
|
||||
let chat = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &chat).await;
|
||||
|
||||
// muted contexts should still show dimmed badge counters eg. in the sidebars,
|
||||
// (same as muted chats show dimmed badge counters in the chatlist)
|
||||
// therefore the fresh messages count should not be affected.
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blobdir_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
assert!(blobdir.is_dir());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_blogdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
tokio::fs::write(&blobdir, b"123").await.unwrap();
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sqlite_parent_not_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let subdir = tmp.path().join("subdir");
|
||||
let dbfile = subdir.join("db.sqlite");
|
||||
let dbfile2 = dbfile.clone();
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(subdir.is_dir());
|
||||
assert!(dbfile2.is_file());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_with_empty_blobdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir(
|
||||
dbfile,
|
||||
blobdir,
|
||||
1,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
Default::default(),
|
||||
);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_with_blobdir_not_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("blobs");
|
||||
let res = Context::with_blobdir(
|
||||
dbfile,
|
||||
blobdir,
|
||||
1,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
Default::default(),
|
||||
);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn no_crashes_on_context_deref() {
|
||||
let t = TestContext::new().await;
|
||||
std::mem::drop(t);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_info() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let info = t.get_info().await.unwrap();
|
||||
assert!(info.contains_key("database_dir"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_info_no_context() {
|
||||
let info = get_info();
|
||||
assert!(info.contains_key("deltachat_core_version"));
|
||||
assert!(!info.contains_key("database_dir"));
|
||||
assert_eq!(info.get("level").unwrap(), "awesome");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_info_completeness() {
|
||||
// For easier debugging,
|
||||
// get_info() shall return all important information configurable by the Config-values.
|
||||
//
|
||||
// There are exceptions for Config-values considered to be unimportant,
|
||||
// too sensitive or summarized in another item.
|
||||
let skip_from_get_info = vec![
|
||||
"addr",
|
||||
"displayname",
|
||||
"imap_certificate_checks",
|
||||
"mail_server",
|
||||
"mail_user",
|
||||
"mail_pw",
|
||||
"mail_port",
|
||||
"mail_security",
|
||||
"notify_about_wrong_pw",
|
||||
"self_reporting_id",
|
||||
"selfstatus",
|
||||
"send_server",
|
||||
"send_user",
|
||||
"send_pw",
|
||||
"send_port",
|
||||
"send_security",
|
||||
"server_flags",
|
||||
"skip_start_messages",
|
||||
"smtp_certificate_checks",
|
||||
"proxy_url", // May contain passwords, don't leak it to the logs.
|
||||
"socks5_enabled", // SOCKS5 options are deprecated.
|
||||
"socks5_host",
|
||||
"socks5_port",
|
||||
"socks5_user",
|
||||
"socks5_password",
|
||||
"key_id",
|
||||
"webxdc_integration",
|
||||
"device_token",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
for key in Config::iter() {
|
||||
let key: String = key.to_string();
|
||||
if !skip_from_get_info.contains(&&*key)
|
||||
&& !key.starts_with("configured")
|
||||
&& !key.starts_with("sys.")
|
||||
{
|
||||
assert!(
|
||||
info.contains_key(&*key),
|
||||
"'{key}' missing in get_info() output"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
|
||||
// Global search finds nothing.
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search in chat with Bob finds nothing.
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Add messages to chat with Bob.
|
||||
let mut msg1 = Message::new_text("foobar".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg1).await?;
|
||||
|
||||
let mut msg2 = Message::new_text("barbaz".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg2).await?;
|
||||
|
||||
alice.send_text(chat.id, "Δ-Chat").await;
|
||||
|
||||
// Global search with a part of text finds the message.
|
||||
let res = alice.search_msgs(None, "ob").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
|
||||
// Global search for "bar" matches both "foobar" and "barbaz".
|
||||
let res = alice.search_msgs(None, "bar").await?;
|
||||
assert_eq!(res.len(), 2);
|
||||
|
||||
// Message added later is returned first.
|
||||
assert_eq!(res.first(), Some(&msg2.id));
|
||||
assert_eq!(res.get(1), Some(&msg1.id));
|
||||
|
||||
// Search is case-insensitive.
|
||||
for chat_id in [None, Some(chat.id)] {
|
||||
let res = alice.search_msgs(chat_id, "δ-chat").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
// Global search with longer text does not find any message.
|
||||
let res = alice.search_msgs(None, "foobarbaz").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search for random string finds nothing.
|
||||
let res = alice.search_msgs(None, "abc").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search in chat with Bob finds the message.
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
|
||||
// Search in Saved Messages does not find the message.
|
||||
let res = alice.search_msgs(Some(self_talk), "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_unaccepted_requests() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: BobBar <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
|
||||
\n\
|
||||
hello bob, foobar test!\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_type(), Chattype::Single);
|
||||
assert!(chat.is_contact_request());
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
chat_id.block(&t).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
|
||||
|
||||
let contact_ids = get_chat_contacts(&t, chat_id).await?;
|
||||
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_limit_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
|
||||
// Add 999 messages
|
||||
let mut msg = Message::new_text("foobar".to_string());
|
||||
for _ in 0..999 {
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
}
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 999);
|
||||
|
||||
// Add one more message, no limit yet
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 1000);
|
||||
|
||||
// Add one more message, that one is truncated then
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 1000);
|
||||
|
||||
// In-chat should not be not limited
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert_eq!(res.len(), 1001);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let context = ContextBuilder::new(dbfile.clone())
|
||||
.with_id(1)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
drop(context);
|
||||
|
||||
let context = ContextBuilder::new(dbfile)
|
||||
.with_id(2)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.is_open().await, false);
|
||||
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
|
||||
assert_eq!(context.open("false".to_string()).await?, false);
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_context_change_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let context = ContextBuilder::new(dbfile)
|
||||
.with_id(1)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
|
||||
context
|
||||
.set_config(Config::Addr, Some("alice@example.org"))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("Failed to change passphrase")?;
|
||||
|
||||
assert_eq!(
|
||||
context.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ongoing() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
// No ongoing process allocated.
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
let receiver = context.alloc_ongoing().await?;
|
||||
|
||||
// Cannot allocate another ongoing process while the first one is running.
|
||||
assert!(context.alloc_ongoing().await.is_err());
|
||||
|
||||
// Stop signal is not sent yet.
|
||||
assert!(receiver.try_recv().is_err());
|
||||
|
||||
assert!(!context.shall_stop_ongoing().await);
|
||||
|
||||
// Send the stop signal.
|
||||
context.stop_ongoing().await;
|
||||
|
||||
// Receive stop signal.
|
||||
receiver.recv().await?;
|
||||
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
// Ongoing process is still running even though stop signal was received,
|
||||
// so another one cannot be allocated.
|
||||
assert!(context.alloc_ongoing().await.is_err());
|
||||
|
||||
context.free_ongoing().await;
|
||||
|
||||
// No ongoing process allocated, should have been stopped already.
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
// Another ongoing process can be allocated now.
|
||||
let _receiver = context.alloc_ongoing().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_next_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
let bob_next_msg_ids = bob.get_next_msgs().await?;
|
||||
assert_eq!(bob_next_msg_ids.len(), 1);
|
||||
assert_eq!(bob_next_msg_ids.first(), Some(&received_msg.id));
|
||||
|
||||
bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
|
||||
.await?;
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
// Next messages include self-sent messages.
|
||||
let alice_next_msg_ids = alice.get_next_msgs().await?;
|
||||
assert_eq!(alice_next_msg_ids.len(), 1);
|
||||
assert_eq!(alice_next_msg_ids.first(), Some(&sent_msg.sender_msg_id));
|
||||
|
||||
alice
|
||||
.set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
|
||||
.await?;
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_draft_self_report() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id = alice.draft_self_report().await?;
|
||||
let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let mut draft = chat_id.get_draft(&alice).await?.unwrap();
|
||||
assert!(draft.text.starts_with("core_version"));
|
||||
|
||||
// Test that sending into the protected chat works:
|
||||
let _sent = alice.send_msg(chat_id, &mut draft).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Change the config circumventing the cache
|
||||
// This simulates what the notification plugin on iOS might do
|
||||
// because it runs in a different process
|
||||
alice
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Alice's Delta Chat doesn't know about it yet:
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Starting IO will fail of course because no server settings are configured,
|
||||
// but it should invalidate the caches:
|
||||
alice.start_io().await;
|
||||
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,649 +0,0 @@
|
||||
use anyhow::Context as _;
|
||||
use strum::IntoEnumIterator;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, MuteDuration};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
use crate::tools::{create_outgoing_rfc724_mid, SystemTime};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_db() -> Result<()> {
|
||||
let tmp = tempfile::tempdir()?;
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
tokio::fs::write(&dbfile, b"123").await?;
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?;
|
||||
|
||||
// Broken database is indistinguishable from encrypted one.
|
||||
assert_eq!(res.is_open().await, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs() {
|
||||
let t = TestContext::new().await;
|
||||
let fresh = t.get_fresh_msgs().await.unwrap();
|
||||
assert!(fresh.is_empty())
|
||||
}
|
||||
|
||||
async fn receive_msg(t: &TestContext, chat: &Chat) {
|
||||
let members = get_chat_contacts(t, chat.id).await.unwrap();
|
||||
let contact = Contact::get_by_id(t, *members.first().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = format!(
|
||||
"From: {}\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <{}>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
contact.get_addr(),
|
||||
create_outgoing_rfc724_mid()
|
||||
);
|
||||
println!("{msg}");
|
||||
receive_imf(t, msg.as_bytes(), false).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs_and_muted_chats() {
|
||||
// receive various mails in 3 chats
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
let claire = t.create_chat_with_contact("", "claire@g.it").await;
|
||||
let dave = t.create_chat_with_contact("", "dave@g.it").await;
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
||||
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 2);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
|
||||
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, dave.id).await.unwrap().len(), 3);
|
||||
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
|
||||
|
||||
// mute one of the chats
|
||||
set_muted(&t, claire.id, MuteDuration::Forever)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
|
||||
|
||||
// receive more messages
|
||||
receive_msg(&t, &bob).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, claire.id).await.unwrap().len(), 3);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
|
||||
|
||||
// unmute claire again
|
||||
set_muted(&t, claire.id, MuteDuration::NotMuted)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_fresh_msgs_and_muted_until() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id).await.unwrap().len(), 1);
|
||||
|
||||
// chat is unmuted by default, here and in the following assert(),
|
||||
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
|
||||
// have the same view to the database.
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
// test get_fresh_msgs() with mute_until in the future
|
||||
set_muted(
|
||||
&t,
|
||||
bob.id,
|
||||
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
// to test get_fresh_msgs() with mute_until in the past,
|
||||
// we need to modify the database directly
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
(time() - 3600, bob.id),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
// test get_fresh_msgs() with "forever" mute_until
|
||||
set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
||||
// that results in "muted forever" by definition.
|
||||
t.sql
|
||||
.execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
assert!(!bob.is_muted());
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_muted_context() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
t.set_config(Config::IsMuted, Some("1")).await?;
|
||||
let chat = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &chat).await;
|
||||
|
||||
// muted contexts should still show dimmed badge counters eg. in the sidebars,
|
||||
// (same as muted chats show dimmed badge counters in the chatlist)
|
||||
// therefore the fresh messages count should not be affected.
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blobdir_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
assert!(blobdir.is_dir());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_blogdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
tokio::fs::write(&blobdir, b"123").await.unwrap();
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sqlite_parent_not_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let subdir = tmp.path().join("subdir");
|
||||
let dbfile = subdir.join("db.sqlite");
|
||||
let dbfile2 = dbfile.clone();
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(subdir.is_dir());
|
||||
assert!(dbfile2.is_file());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_with_empty_blobdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir(
|
||||
dbfile,
|
||||
blobdir,
|
||||
1,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
Default::default(),
|
||||
);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_with_blobdir_not_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("blobs");
|
||||
let res = Context::with_blobdir(
|
||||
dbfile,
|
||||
blobdir,
|
||||
1,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
Default::default(),
|
||||
);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn no_crashes_on_context_deref() {
|
||||
let t = TestContext::new().await;
|
||||
std::mem::drop(t);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_info() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let info = t.get_info().await.unwrap();
|
||||
assert!(info.contains_key("database_dir"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_info_no_context() {
|
||||
let info = get_info();
|
||||
assert!(info.contains_key("deltachat_core_version"));
|
||||
assert!(!info.contains_key("database_dir"));
|
||||
assert_eq!(info.get("level").unwrap(), "awesome");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_info_completeness() {
|
||||
// For easier debugging,
|
||||
// get_info() shall return all important information configurable by the Config-values.
|
||||
//
|
||||
// There are exceptions for Config-values considered to be unimportant,
|
||||
// too sensitive or summarized in another item.
|
||||
let skip_from_get_info = vec![
|
||||
"addr",
|
||||
"displayname",
|
||||
"imap_certificate_checks",
|
||||
"mail_server",
|
||||
"mail_user",
|
||||
"mail_pw",
|
||||
"mail_port",
|
||||
"mail_security",
|
||||
"notify_about_wrong_pw",
|
||||
"self_reporting_id",
|
||||
"selfstatus",
|
||||
"send_server",
|
||||
"send_user",
|
||||
"send_pw",
|
||||
"send_port",
|
||||
"send_security",
|
||||
"server_flags",
|
||||
"skip_start_messages",
|
||||
"smtp_certificate_checks",
|
||||
"proxy_url", // May contain passwords, don't leak it to the logs.
|
||||
"socks5_enabled", // SOCKS5 options are deprecated.
|
||||
"socks5_host",
|
||||
"socks5_port",
|
||||
"socks5_user",
|
||||
"socks5_password",
|
||||
"key_id",
|
||||
"webxdc_integration",
|
||||
"device_token",
|
||||
"encrypted_device_token",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
for key in Config::iter() {
|
||||
let key: String = key.to_string();
|
||||
if !skip_from_get_info.contains(&&*key)
|
||||
&& !key.starts_with("configured")
|
||||
&& !key.starts_with("sys.")
|
||||
{
|
||||
assert!(
|
||||
info.contains_key(&*key),
|
||||
"'{key}' missing in get_info() output"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
|
||||
// Global search finds nothing.
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search in chat with Bob finds nothing.
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Add messages to chat with Bob.
|
||||
let mut msg1 = Message::new_text("foobar".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg1).await?;
|
||||
|
||||
let mut msg2 = Message::new_text("barbaz".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg2).await?;
|
||||
|
||||
alice.send_text(chat.id, "Δ-Chat").await;
|
||||
|
||||
// Global search with a part of text finds the message.
|
||||
let res = alice.search_msgs(None, "ob").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
|
||||
// Global search for "bar" matches both "foobar" and "barbaz".
|
||||
let res = alice.search_msgs(None, "bar").await?;
|
||||
assert_eq!(res.len(), 2);
|
||||
|
||||
// Message added later is returned first.
|
||||
assert_eq!(res.first(), Some(&msg2.id));
|
||||
assert_eq!(res.get(1), Some(&msg1.id));
|
||||
|
||||
// Search is case-insensitive.
|
||||
for chat_id in [None, Some(chat.id)] {
|
||||
let res = alice.search_msgs(chat_id, "δ-chat").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
// Global search with longer text does not find any message.
|
||||
let res = alice.search_msgs(None, "foobarbaz").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search for random string finds nothing.
|
||||
let res = alice.search_msgs(None, "abc").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
// Search in chat with Bob finds the message.
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
|
||||
// Search in Saved Messages does not find the message.
|
||||
let res = alice.search_msgs(Some(self_talk), "foo").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_unaccepted_requests() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: BobBar <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
|
||||
\n\
|
||||
hello bob, foobar test!\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_type(), Chattype::Single);
|
||||
assert!(chat.is_contact_request());
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
chat_id.block(&t).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
|
||||
|
||||
let contact_ids = get_chat_contacts(&t, chat_id).await?;
|
||||
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_limit_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
|
||||
// Add 999 messages
|
||||
let mut msg = Message::new_text("foobar".to_string());
|
||||
for _ in 0..999 {
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
}
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 999);
|
||||
|
||||
// Add one more message, no limit yet
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 1000);
|
||||
|
||||
// Add one more message, that one is truncated then
|
||||
send_msg(&alice, chat.id, &mut msg).await?;
|
||||
let res = alice.search_msgs(None, "foo").await?;
|
||||
assert_eq!(res.len(), 1000);
|
||||
|
||||
// In-chat should not be not limited
|
||||
let res = alice.search_msgs(Some(chat.id), "foo").await?;
|
||||
assert_eq!(res.len(), 1001);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let context = ContextBuilder::new(dbfile.clone())
|
||||
.with_id(1)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
drop(context);
|
||||
|
||||
let context = ContextBuilder::new(dbfile)
|
||||
.with_id(2)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.is_open().await, false);
|
||||
assert_eq!(context.check_passphrase("bar".to_string()).await?, false);
|
||||
assert_eq!(context.open("false".to_string()).await?, false);
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_context_change_passphrase() -> Result<()> {
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let context = ContextBuilder::new(dbfile)
|
||||
.with_id(1)
|
||||
.build()
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
assert_eq!(context.is_open().await, true);
|
||||
|
||||
context
|
||||
.set_config(Config::Addr, Some("alice@example.org"))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("Failed to change passphrase")?;
|
||||
|
||||
assert_eq!(
|
||||
context.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ongoing() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
// No ongoing process allocated.
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
let receiver = context.alloc_ongoing().await?;
|
||||
|
||||
// Cannot allocate another ongoing process while the first one is running.
|
||||
assert!(context.alloc_ongoing().await.is_err());
|
||||
|
||||
// Stop signal is not sent yet.
|
||||
assert!(receiver.try_recv().is_err());
|
||||
|
||||
assert!(!context.shall_stop_ongoing().await);
|
||||
|
||||
// Send the stop signal.
|
||||
context.stop_ongoing().await;
|
||||
|
||||
// Receive stop signal.
|
||||
receiver.recv().await?;
|
||||
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
// Ongoing process is still running even though stop signal was received,
|
||||
// so another one cannot be allocated.
|
||||
assert!(context.alloc_ongoing().await.is_err());
|
||||
|
||||
context.free_ongoing().await;
|
||||
|
||||
// No ongoing process allocated, should have been stopped already.
|
||||
assert!(context.shall_stop_ongoing().await);
|
||||
|
||||
// Another ongoing process can be allocated now.
|
||||
let _receiver = context.alloc_ongoing().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_next_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
let bob_next_msg_ids = bob.get_next_msgs().await?;
|
||||
assert_eq!(bob_next_msg_ids.len(), 1);
|
||||
assert_eq!(bob_next_msg_ids.first(), Some(&received_msg.id));
|
||||
|
||||
bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
|
||||
.await?;
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
// Next messages include self-sent messages.
|
||||
let alice_next_msg_ids = alice.get_next_msgs().await?;
|
||||
assert_eq!(alice_next_msg_ids.len(), 1);
|
||||
assert_eq!(alice_next_msg_ids.first(), Some(&sent_msg.sender_msg_id));
|
||||
|
||||
alice
|
||||
.set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
|
||||
.await?;
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_draft_self_report() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id = alice.draft_self_report().await?;
|
||||
let msg = get_chat_msg(&alice, chat_id, 0, 1).await;
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let mut draft = chat_id.get_draft(&alice).await?.unwrap();
|
||||
assert!(draft.text.starts_with("core_version"));
|
||||
|
||||
// Test that sending into the protected chat works:
|
||||
let _sent = alice.send_msg(chat_id, &mut draft).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cache_is_cleared_when_io_is_started() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Change the config circumventing the cache
|
||||
// This simulates what the notification plugin on iOS might do
|
||||
// because it runs in a different process
|
||||
alice
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('show_emails', '0')",
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Alice's Delta Chat doesn't know about it yet:
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("2".to_string())
|
||||
);
|
||||
|
||||
// Starting IO will fail of course because no server settings are configured,
|
||||
// but it should invalidate the caches:
|
||||
alice.start_io().await;
|
||||
|
||||
assert_eq!(
|
||||
alice.get_config(Config::ShowEmails).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -100,9 +100,7 @@ pub async fn maybe_set_logging_xdc(
|
||||
context,
|
||||
msg.get_viewtype(),
|
||||
chat_id,
|
||||
msg.param
|
||||
.get_path(Param::Filename, context)
|
||||
.unwrap_or_default(),
|
||||
msg.param.get_path(Param::File, context).unwrap_or_default(),
|
||||
msg.get_id(),
|
||||
)
|
||||
.await?;
|
||||
@@ -115,11 +113,11 @@ pub async fn maybe_set_logging_xdc_inner(
|
||||
context: &Context,
|
||||
viewtype: Viewtype,
|
||||
chat_id: ChatId,
|
||||
filename: Option<PathBuf>,
|
||||
file: Option<PathBuf>,
|
||||
msg_id: MsgId,
|
||||
) -> anyhow::Result<()> {
|
||||
if viewtype == Viewtype::Webxdc {
|
||||
if let Some(file) = filename {
|
||||
if let Some(file) = file {
|
||||
if let Some(file_name) = file.file_name().and_then(|name| name.to_str()) {
|
||||
if file_name.starts_with("debug_logging")
|
||||
&& file_name.ends_with(".xdc")
|
||||
|
||||
@@ -440,7 +440,7 @@ mod tests {
|
||||
let file = alice.get_blobdir().join("minimal.xdc");
|
||||
tokio::fs::write(&file, include_bytes!("../test-data/webxdc/minimal.xdc")).await?;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
instance.set_file_and_deduplicate(&alice, &file, None, None)?;
|
||||
instance.set_file(file.to_str().unwrap(), None);
|
||||
let _sent1 = alice.send_msg(chat_id, &mut instance).await;
|
||||
|
||||
alice
|
||||
|
||||
806
src/ephemeral.rs
806
src/ephemeral.rs
@@ -713,4 +713,808 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ephemeral_tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{marknoticed_chat, set_muted, ChatVisibility, MuteDuration};
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_ARCHIVED_LINK;
|
||||
use crate::download::DownloadState;
|
||||
use crate::location;
|
||||
use crate::message::markseen_msgs;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
|
||||
use crate::{
|
||||
chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus},
|
||||
tools::IsNoneOrEmpty,
|
||||
};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_ephemeral_messages() {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await,
|
||||
"You disabled message deletion timer."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 1 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 30 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 30 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 60 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 minute."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 90 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1.5 minutes."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 30 * 60 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 30 minutes."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 60 * 60 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 hour."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 5400 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1.5 hours."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 2 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 2 hours."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 day."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 2 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 2 days."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 7 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 week."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 4 * 7 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 4 weeks."
|
||||
);
|
||||
}
|
||||
|
||||
/// Test enabling and disabling ephemeral timer remotely.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that enabling ephemeral timer in unpromoted group does not send a message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_unpromoted() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?;
|
||||
|
||||
// Group is unpromoted, the timer can be changed without sending a message.
|
||||
assert!(chat_id.is_unpromoted(&alice).await?);
|
||||
chat_id
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_none());
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Promote the group.
|
||||
send_text_msg(&alice, chat_id, "hi!".to_string()).await?;
|
||||
assert!(chat_id.is_promoted(&alice).await?);
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_some());
|
||||
|
||||
chat_id
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_some());
|
||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that timer is enabled even if the message explicitly enabling the timer is lost.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_enable_lost() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// Alice enables the timer.
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
// The message enabling the timer is lost.
|
||||
let _sent = alice.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled,
|
||||
);
|
||||
|
||||
// Alice sends a text message.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives text message and enables the timer, even though explicit timer update was
|
||||
// lost previously.
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the
|
||||
/// timer does not result in disabling the timer on the Bob's side.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_timer_rollback() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// Alice sends message to Bob
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
// Alice sends second message to Bob, with no timer
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
// Bob sets ephemeral timer and sends a message about timer change
|
||||
chat_bob
|
||||
.set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent_timer_change = bob.pop_sent_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Bob receives message from Alice.
|
||||
// Alice message has no timer. However, Bob should not disable timer,
|
||||
// because Alice replies to old message.
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Alice receives message from Bob
|
||||
alice.recv_msg(&sent_timer_change).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Bob disables the chat timer.
|
||||
// Note that the last message in the Bob's chat is from Alice and has no timer,
|
||||
// but the chat timer is enabled.
|
||||
chat_bob
|
||||
.set_ephemeral_timer(&bob.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_delete_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let self_chat = t.get_self_chat().await;
|
||||
|
||||
assert_eq!(next_expiration_timestamp(&t).await, None);
|
||||
|
||||
t.send_text(self_chat.id, "Saved message, which we delete manually")
|
||||
.await;
|
||||
let msg = t.get_last_msg_in(self_chat.id).await;
|
||||
msg.id.trash(&t, false).await?;
|
||||
check_msg_is_deleted(&t, &self_chat, msg.id).await;
|
||||
|
||||
self_chat
|
||||
.id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send a saved message which will be deleted after 3600s
|
||||
let now = time();
|
||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Set DeleteDeviceAfter to 1800s. Then send a saved message which will
|
||||
// still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
|
||||
t.set_config(Config::DeleteDeviceAfter, Some("1800"))
|
||||
.await?;
|
||||
|
||||
let now = time();
|
||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter.
|
||||
let bob_chat = t.create_chat_with_contact("", "bob@example.net").await;
|
||||
let now = time();
|
||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(
|
||||
&t,
|
||||
msg.sender_msg_id,
|
||||
&bob_chat,
|
||||
now + 1799,
|
||||
// The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and
|
||||
// therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later.
|
||||
time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Enable ephemeral messages with Bob -> message will be deleted after 60s.
|
||||
// This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time).
|
||||
bob_chat
|
||||
.id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
|
||||
let now = time();
|
||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_msg_will_be_deleted(
|
||||
t: &TestContext,
|
||||
msg_id: MsgId,
|
||||
chat: &Chat,
|
||||
not_deleted_at: i64,
|
||||
deleted_at: i64,
|
||||
) -> Result<()> {
|
||||
let next_expiration = next_expiration_timestamp(t).await.unwrap();
|
||||
|
||||
assert!(next_expiration > not_deleted_at);
|
||||
delete_expired_messages(t, not_deleted_at).await?;
|
||||
|
||||
let loaded = Message::load_from_db(t, msg_id).await?;
|
||||
assert!(!loaded.text.is_empty());
|
||||
assert_eq!(loaded.chat_id, chat.id);
|
||||
|
||||
assert!(next_expiration < deleted_at);
|
||||
delete_expired_messages(t, deleted_at).await?;
|
||||
t.evtracker
|
||||
.get_matching(|evt| {
|
||||
if let EventType::MsgDeleted {
|
||||
msg_id: event_msg_id,
|
||||
..
|
||||
} = evt
|
||||
{
|
||||
*event_msg_id == msg_id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let loaded = Message::load_from_db_optional(t, msg_id).await?;
|
||||
assert!(loaded.is_none());
|
||||
|
||||
// Check that the msg was deleted locally.
|
||||
check_msg_is_deleted(t, chat, msg_id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||
let chat_items = chat::get_chat_msgs(t, chat.id).await.unwrap();
|
||||
// Check that the chat is empty except for possibly info messages:
|
||||
for item in &chat_items {
|
||||
if let ChatItem::Message { msg_id } = item {
|
||||
let msg = Message::load_from_db(t, *msg_id).await.unwrap();
|
||||
assert!(msg.is_info())
|
||||
}
|
||||
}
|
||||
|
||||
// Check that if there is a message left, the text and metadata are gone
|
||||
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
|
||||
assert_eq!(msg.from_id, ContactId::UNDEFINED);
|
||||
assert_eq!(msg.to_id, ContactId::UNDEFINED);
|
||||
assert_eq!(msg.text, "");
|
||||
let rawtxt: Option<String> = t
|
||||
.sql
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
for (id, timestamp, ephemeral_timestamp) in &[
|
||||
(900, now - 2 * HOUR, 0),
|
||||
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
||||
(1010, now - 23 * HOUR, 0),
|
||||
(1020, now - 21 * HOUR, 0),
|
||||
(1030, now - 19 * HOUR, 0),
|
||||
(2000, now - 18 * HOUR, now - HOUR),
|
||||
(2020, now - 17 * HOUR, now + HOUR),
|
||||
(3000, now + HOUR, 0),
|
||||
] {
|
||||
let message_id = id.to_string();
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
(id, &message_id, timestamp, ephemeral_timestamp),
|
||||
)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
||||
(&message_id, id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
||||
assert_eq!(
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
(id.to_string(),),
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This should mark message 2000 for deletion.
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 2000).await?;
|
||||
remove_uid(&t, 2000).await?;
|
||||
// No other messages are marked for deletion.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?;
|
||||
|
||||
MsgId::new(1000)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ())
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
||||
remove_uid(&t, 1000).await?;
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1010).await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ())
|
||||
.await?;
|
||||
|
||||
MsgId::new(1010)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
// Keep downloadable for now.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 3000).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Regression test for a bug in the timer rollback protection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_timer_references() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Message with Message-ID <first@example.com> and no timer is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <first@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||
|
||||
// Message with Message-ID <second@example.com> is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <second@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:11:00 +0000\n\
|
||||
Ephemeral-Timer: 60\n\
|
||||
\n\
|
||||
second message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
// Message is deleted when its timer expires.
|
||||
msg.id.trash(&alice, false).await?;
|
||||
|
||||
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
|
||||
// <second@example.com>, is received. The message <second@example.come> is not in the
|
||||
// database anymore, so the timer should be applied unconditionally without rollback
|
||||
// protection.
|
||||
//
|
||||
// Previously Delta Chat fallen back to using <first@example.com> in this case and
|
||||
// compared received timer value to the timer value of the <first@example.com>. Because
|
||||
// their timer values are the same ("disabled"), Delta Chat assumed that the timer was not
|
||||
// changed explicitly and the change should be ignored.
|
||||
//
|
||||
// The message also contains a quote of the first message to test that only References:
|
||||
// header and not In-Reply-To: is consulted by the rollback protection.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <third@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:12:00 +0000\n\
|
||||
References: <first@example.com> <second@example.com>\n\
|
||||
In-Reply-To: <first@example.com>\n\
|
||||
\n\
|
||||
> hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(
|
||||
msg.chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Tests that if we are offline for a time longer than the ephemeral timer duration, the message
|
||||
// is deleted from the chat but is still in the "smtp" table, i.e. will be sent upon a
|
||||
// successful reconnection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_msg_offline() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
let now = time();
|
||||
check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1)
|
||||
.await?;
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that POI location is deleted when ephemeral message expires.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_poi_location() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
let mut poi_msg = Message::new_text("Here".to_string());
|
||||
poi_msg.set_location(10.0, 20.0);
|
||||
|
||||
let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await;
|
||||
let bob_received_message = bob.recv_msg(&alice_sent_message).await;
|
||||
markseen_msgs(bob, vec![bob_received_message.id]).await?;
|
||||
|
||||
for account in [alice, bob] {
|
||||
let locations = location::get_range(account, None, None, 0, 0).await?;
|
||||
assert_eq!(locations.len(), 1);
|
||||
}
|
||||
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
for account in [alice, bob] {
|
||||
delete_expired_messages(account, time()).await?;
|
||||
let locations = location::get_range(account, None, None, 0, 0).await?;
|
||||
assert_eq!(locations.len(), 0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that `.get_ephemeral_timer()` returns an error for invalid chat ID.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_ephemeral_timer_wrong_chat_id() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
let chat_id = ChatId::new(12345);
|
||||
assert!(chat_id.get_ephemeral_timer(&context).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that ephemeral timer is started when the chat is noticed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_noticed_ephemeral_timer() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
||||
|
||||
marknoticed_chat(bob, bob_received_message.chat_id).await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
||||
.await?
|
||||
.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that archiving the chat starts ephemeral timer.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archived_ephemeral_timer() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
||||
|
||||
bob_received_message
|
||||
.chat_id
|
||||
.set_visibility(bob, ChatVisibility::Archived)
|
||||
.await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
||||
.await?
|
||||
.is_none());
|
||||
|
||||
// Bob mutes the chat so it is not unarchived.
|
||||
set_muted(bob, bob_received_message.chat_id, MuteDuration::Forever).await?;
|
||||
|
||||
// Now test that for already archived chat
|
||||
// timer is started if all archived chats are marked as noticed.
|
||||
let bob_received_message_2 = tcm.send_recv(alice, bob, "Hello again!").await;
|
||||
assert_eq!(bob_received_message_2.state, MessageState::InFresh);
|
||||
|
||||
marknoticed_chat(bob, DC_CHAT_ID_ARCHIVED_LINK).await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(
|
||||
Message::load_from_db_optional(bob, bob_received_message_2.id)
|
||||
.await?
|
||||
.is_none()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,781 +0,0 @@
|
||||
use super::*;
|
||||
use crate::chat::{marknoticed_chat, set_muted, ChatVisibility, MuteDuration};
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_ARCHIVED_LINK;
|
||||
use crate::download::DownloadState;
|
||||
use crate::location;
|
||||
use crate::message::markseen_msgs;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::timesmearing::MAX_SECONDS_TO_LEND_FROM_FUTURE;
|
||||
use crate::{
|
||||
chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus},
|
||||
tools::IsNoneOrEmpty,
|
||||
};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_ephemeral_messages() {
|
||||
let context = TestContext::new().await;
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await,
|
||||
"You disabled message deletion timer."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 1 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 30 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 1 minute."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 90 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 1.5 minutes."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 30 * 60 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 30 minutes."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled { duration: 60 * 60 },
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 hour."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 5400 }, ContactId::SELF)
|
||||
.await,
|
||||
"You set message deletion timer to 1.5 hours."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 2 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 2 hours."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 day."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 2 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 2 days."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 7 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 1 week."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
&context,
|
||||
Timer::Enabled {
|
||||
duration: 4 * 7 * 24 * 60 * 60
|
||||
},
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"You set message deletion timer to 4 weeks."
|
||||
);
|
||||
}
|
||||
|
||||
/// Test enabling and disabling ephemeral timer remotely.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that enabling ephemeral timer in unpromoted group does not send a message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_unpromoted() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?;
|
||||
|
||||
// Group is unpromoted, the timer can be changed without sending a message.
|
||||
assert!(chat_id.is_unpromoted(&alice).await?);
|
||||
chat_id
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_none());
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Promote the group.
|
||||
send_text_msg(&alice, chat_id, "hi!".to_string()).await?;
|
||||
assert!(chat_id.is_promoted(&alice).await?);
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_some());
|
||||
|
||||
chat_id
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await;
|
||||
assert!(sent.is_some());
|
||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that timer is enabled even if the message explicitly enabling the timer is lost.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_enable_lost() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// Alice enables the timer.
|
||||
chat_alice
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
// The message enabling the timer is lost.
|
||||
let _sent = alice.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled,
|
||||
);
|
||||
|
||||
// Alice sends a text message.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives text message and enables the timer, even though explicit timer update was
|
||||
// lost previously.
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the
|
||||
/// timer does not result in disabling the timer on the Bob's side.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_timer_rollback() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// Alice sends message to Bob
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
// Alice sends second message to Bob, with no timer
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
// Bob sets ephemeral timer and sends a message about timer change
|
||||
chat_bob
|
||||
.set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent_timer_change = bob.pop_sent_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Bob receives message from Alice.
|
||||
// Alice message has no timer. However, Bob should not disable timer,
|
||||
// because Alice replies to old message.
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Alice receives message from Bob
|
||||
alice.recv_msg(&sent_timer_change).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
// Bob disables the chat timer.
|
||||
// Note that the last message in the Bob's chat is from Alice and has no timer,
|
||||
// but the chat timer is enabled.
|
||||
chat_bob
|
||||
.set_ephemeral_timer(&bob.ctx, Timer::Disabled)
|
||||
.await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(
|
||||
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_delete_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let self_chat = t.get_self_chat().await;
|
||||
|
||||
assert_eq!(next_expiration_timestamp(&t).await, None);
|
||||
|
||||
t.send_text(self_chat.id, "Saved message, which we delete manually")
|
||||
.await;
|
||||
let msg = t.get_last_msg_in(self_chat.id).await;
|
||||
msg.id.trash(&t, false).await?;
|
||||
check_msg_is_deleted(&t, &self_chat, msg.id).await;
|
||||
|
||||
self_chat
|
||||
.id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send a saved message which will be deleted after 3600s
|
||||
let now = time();
|
||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Set DeleteDeviceAfter to 1800s. Then send a saved message which will
|
||||
// still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
|
||||
t.set_config(Config::DeleteDeviceAfter, Some("1800"))
|
||||
.await?;
|
||||
|
||||
let now = time();
|
||||
let msg = t.send_text(self_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter.
|
||||
let bob_chat = t.create_chat_with_contact("", "bob@example.net").await;
|
||||
let now = time();
|
||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(
|
||||
&t,
|
||||
msg.sender_msg_id,
|
||||
&bob_chat,
|
||||
now + 1799,
|
||||
// The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and
|
||||
// therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later.
|
||||
time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Enable ephemeral messages with Bob -> message will be deleted after 60s.
|
||||
// This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time).
|
||||
bob_chat
|
||||
.id
|
||||
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
|
||||
let now = time();
|
||||
let msg = t.send_text(bob_chat.id, "Message text").await;
|
||||
|
||||
check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_msg_will_be_deleted(
|
||||
t: &TestContext,
|
||||
msg_id: MsgId,
|
||||
chat: &Chat,
|
||||
not_deleted_at: i64,
|
||||
deleted_at: i64,
|
||||
) -> Result<()> {
|
||||
let next_expiration = next_expiration_timestamp(t).await.unwrap();
|
||||
|
||||
assert!(next_expiration > not_deleted_at);
|
||||
delete_expired_messages(t, not_deleted_at).await?;
|
||||
|
||||
let loaded = Message::load_from_db(t, msg_id).await?;
|
||||
assert!(!loaded.text.is_empty());
|
||||
assert_eq!(loaded.chat_id, chat.id);
|
||||
|
||||
assert!(next_expiration < deleted_at);
|
||||
delete_expired_messages(t, deleted_at).await?;
|
||||
t.evtracker
|
||||
.get_matching(|evt| {
|
||||
if let EventType::MsgDeleted {
|
||||
msg_id: event_msg_id,
|
||||
..
|
||||
} = evt
|
||||
{
|
||||
*event_msg_id == msg_id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let loaded = Message::load_from_db_optional(t, msg_id).await?;
|
||||
assert!(loaded.is_none());
|
||||
|
||||
// Check that the msg was deleted locally.
|
||||
check_msg_is_deleted(t, chat, msg_id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||
let chat_items = chat::get_chat_msgs(t, chat.id).await.unwrap();
|
||||
// Check that the chat is empty except for possibly info messages:
|
||||
for item in &chat_items {
|
||||
if let ChatItem::Message { msg_id } = item {
|
||||
let msg = Message::load_from_db(t, *msg_id).await.unwrap();
|
||||
assert!(msg.is_info())
|
||||
}
|
||||
}
|
||||
|
||||
// Check that if there is a message left, the text and metadata are gone
|
||||
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
|
||||
assert_eq!(msg.from_id, ContactId::UNDEFINED);
|
||||
assert_eq!(msg.to_id, ContactId::UNDEFINED);
|
||||
assert_eq!(msg.text, "");
|
||||
let rawtxt: Option<String> = t
|
||||
.sql
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
for (id, timestamp, ephemeral_timestamp) in &[
|
||||
(900, now - 2 * HOUR, 0),
|
||||
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
||||
(1010, now - 23 * HOUR, 0),
|
||||
(1020, now - 21 * HOUR, 0),
|
||||
(1030, now - 19 * HOUR, 0),
|
||||
(2000, now - 18 * HOUR, now - HOUR),
|
||||
(2020, now - 17 * HOUR, now + HOUR),
|
||||
(3000, now + HOUR, 0),
|
||||
] {
|
||||
let message_id = id.to_string();
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
(id, &message_id, timestamp, ephemeral_timestamp),
|
||||
)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
||||
(&message_id, id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
|
||||
assert_eq!(
|
||||
context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
(id.to_string(),),
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This should mark message 2000 for deletion.
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 2000).await?;
|
||||
remove_uid(&t, 2000).await?;
|
||||
// No other messages are marked for deletion.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?;
|
||||
|
||||
MsgId::new(1000)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1000'", ())
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
|
||||
remove_uid(&t, 1000).await?;
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 1010).await?;
|
||||
t.sql
|
||||
.execute("UPDATE imap SET target=folder WHERE rfc724_mid='1010'", ())
|
||||
.await?;
|
||||
|
||||
MsgId::new(1010)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
// Keep downloadable for now.
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.count("SELECT COUNT(*) FROM imap WHERE target=''", ())
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
||||
delete_expired_imap_messages(&t).await?;
|
||||
test_marked_for_deletion(&t, 3000).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Regression test for a bug in the timer rollback protection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_timer_references() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Message with Message-ID <first@example.com> and no timer is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <first@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
|
||||
|
||||
// Message with Message-ID <second@example.com> is received.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <second@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:11:00 +0000\n\
|
||||
Ephemeral-Timer: 60\n\
|
||||
\n\
|
||||
second message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
// Message is deleted when its timer expires.
|
||||
msg.id.trash(&alice, false).await?;
|
||||
|
||||
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
|
||||
// <second@example.com>, is received. The message <second@example.come> is not in the
|
||||
// database anymore, so the timer should be applied unconditionally without rollback
|
||||
// protection.
|
||||
//
|
||||
// Previously Delta Chat fallen back to using <first@example.com> in this case and
|
||||
// compared received timer value to the timer value of the <first@example.com>. Because
|
||||
// their timer values are the same ("disabled"), Delta Chat assumed that the timer was not
|
||||
// changed explicitly and the change should be ignored.
|
||||
//
|
||||
// The message also contains a quote of the first message to test that only References:
|
||||
// header and not In-Reply-To: is consulted by the rollback protection.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: Subject\n\
|
||||
Message-ID: <third@example.com>\n\
|
||||
Date: Sun, 22 Mar 2020 00:12:00 +0000\n\
|
||||
References: <first@example.com> <second@example.com>\n\
|
||||
In-Reply-To: <first@example.com>\n\
|
||||
\n\
|
||||
> hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(
|
||||
msg.chat_id.get_ephemeral_timer(&alice).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Tests that if we are offline for a time longer than the ephemeral timer duration, the message
|
||||
// is deleted from the chat but is still in the "smtp" table, i.e. will be sent upon a
|
||||
// successful reconnection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_msg_offline() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
let now = time();
|
||||
check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1).await?;
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that POI location is deleted when ephemeral message expires.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_poi_location() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
let mut poi_msg = Message::new_text("Here".to_string());
|
||||
poi_msg.set_location(10.0, 20.0);
|
||||
|
||||
let alice_sent_message = alice.send_msg(chat.id, &mut poi_msg).await;
|
||||
let bob_received_message = bob.recv_msg(&alice_sent_message).await;
|
||||
markseen_msgs(bob, vec![bob_received_message.id]).await?;
|
||||
|
||||
for account in [alice, bob] {
|
||||
let locations = location::get_range(account, None, None, 0, 0).await?;
|
||||
assert_eq!(locations.len(), 1);
|
||||
}
|
||||
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
for account in [alice, bob] {
|
||||
delete_expired_messages(account, time()).await?;
|
||||
let locations = location::get_range(account, None, None, 0, 0).await?;
|
||||
assert_eq!(locations.len(), 0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that `.get_ephemeral_timer()` returns an error for invalid chat ID.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_ephemeral_timer_wrong_chat_id() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
let chat_id = ChatId::new(12345);
|
||||
assert!(chat_id.get_ephemeral_timer(&context).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that ephemeral timer is started when the chat is noticed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_noticed_ephemeral_timer() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
||||
|
||||
marknoticed_chat(bob, bob_received_message.chat_id).await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
||||
.await?
|
||||
.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that archiving the chat starts ephemeral timer.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_archived_ephemeral_timer() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let bob_received_message = tcm.send_recv(alice, bob, "Hello!").await;
|
||||
|
||||
bob_received_message
|
||||
.chat_id
|
||||
.set_visibility(bob, ChatVisibility::Archived)
|
||||
.await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(Message::load_from_db_optional(bob, bob_received_message.id)
|
||||
.await?
|
||||
.is_none());
|
||||
|
||||
// Bob mutes the chat so it is not unarchived.
|
||||
set_muted(bob, bob_received_message.chat_id, MuteDuration::Forever).await?;
|
||||
|
||||
// Now test that for already archived chat
|
||||
// timer is started if all archived chats are marked as noticed.
|
||||
let bob_received_message_2 = tcm.send_recv(alice, bob, "Hello again!").await;
|
||||
assert_eq!(bob_received_message_2.state, MessageState::InFresh);
|
||||
|
||||
marknoticed_chat(bob, DC_CHAT_ID_ARCHIVED_LINK).await?;
|
||||
SystemTime::shift(Duration::from_secs(100));
|
||||
|
||||
delete_expired_messages(bob, time()).await?;
|
||||
|
||||
assert!(
|
||||
Message::load_from_db_optional(bob, bob_received_message_2.id)
|
||||
.await?
|
||||
.is_none()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -97,9 +97,6 @@ pub enum EventType {
|
||||
|
||||
/// Reactions for the message changed.
|
||||
IncomingReaction {
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
|
||||
/// ID of the contact whose reaction set is changed.
|
||||
contact_id: ContactId,
|
||||
|
||||
|
||||
37
src/html.rs
37
src/html.rs
@@ -291,7 +291,7 @@ pub fn new_html_mimepart(html: String) -> PartBuilder {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat;
|
||||
use crate::chat::{forward_msgs, save_msgs};
|
||||
use crate::chat::forward_msgs;
|
||||
use crate::config::Config;
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{MessengerMessage, Viewtype};
|
||||
@@ -371,6 +371,7 @@ and will be wrapped as usual.<br/>
|
||||
mime-modified should not be set set as there is no html and no special stuff;<br/>
|
||||
although not being a delta-message.<br/>
|
||||
test some special html-characters as < > and & but also " and ' :)<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"#
|
||||
);
|
||||
@@ -404,6 +405,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
r##"<html>
|
||||
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
|
||||
</html>
|
||||
|
||||
"##
|
||||
);
|
||||
}
|
||||
@@ -420,6 +422,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
this is <b>html</b>
|
||||
</p>
|
||||
</html>
|
||||
|
||||
"##
|
||||
);
|
||||
}
|
||||
@@ -496,38 +499,6 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_html_save_msg() -> Result<()> {
|
||||
// Alice receives a non-delta html-message
|
||||
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?;
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
|
||||
// Alice saves the message
|
||||
let self_chat = alice.get_self_chat().await;
|
||||
save_msgs(&alice, &[msg.id]).await?;
|
||||
let saved_msg = alice.get_last_msg_in(self_chat.get_id()).await;
|
||||
assert_ne!(saved_msg.id, msg.id);
|
||||
assert_eq!(
|
||||
saved_msg.get_original_msg_id(&alice).await?.unwrap(),
|
||||
msg.id
|
||||
);
|
||||
assert!(!saved_msg.is_forwarded()); // UI should not flag "saved messages" as "forwarded"
|
||||
assert_ne!(saved_msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(saved_msg.get_from_id(), msg.get_from_id());
|
||||
assert_eq!(saved_msg.is_dc_message, MessengerMessage::No);
|
||||
assert!(saved_msg.get_text().contains("this is plain"));
|
||||
assert!(saved_msg.has_html());
|
||||
let html = saved_msg.get_id().get_html(&alice).await?.unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_html_forwarding_encrypted() {
|
||||
// Alice receives a non-delta html-message
|
||||
|
||||
410
src/imap.rs
410
src/imap.rs
@@ -1331,7 +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)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn fetch_many_msgs(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -1589,15 +1589,15 @@ impl Session {
|
||||
};
|
||||
|
||||
if self.can_metadata() && self.can_push() {
|
||||
let old_encrypted_device_token =
|
||||
context.get_config(Config::EncryptedDeviceToken).await?;
|
||||
let device_token_changed =
|
||||
context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
|
||||
|
||||
// Whether we need to update encrypted device token.
|
||||
let device_token_changed = old_encrypted_device_token.is_none()
|
||||
|| context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
|
||||
|
||||
let new_encrypted_device_token;
|
||||
if device_token_changed {
|
||||
let folder = context
|
||||
.get_config(Config::ConfiguredInboxFolder)
|
||||
.await?
|
||||
.context("INBOX is not configured")?;
|
||||
|
||||
let encrypted_device_token = encrypt_device_token(&device_token)
|
||||
.context("Failed to encrypt device token")?;
|
||||
|
||||
@@ -1606,23 +1606,22 @@ impl Session {
|
||||
// <https://www.rfc-editor.org/rfc/rfc7888>.
|
||||
let encrypted_device_token_len = encrypted_device_token.len();
|
||||
|
||||
// Store device token saved on the server
|
||||
// to prevent storing duplicate tokens.
|
||||
// The server cannot deduplicate on its own
|
||||
// because encryption gives a different
|
||||
// result each time.
|
||||
context
|
||||
.set_config_internal(Config::DeviceToken, Some(&device_token))
|
||||
.await?;
|
||||
context
|
||||
.set_config_internal(
|
||||
Config::EncryptedDeviceToken,
|
||||
Some(&encrypted_device_token),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if encrypted_device_token_len <= 4096 {
|
||||
new_encrypted_device_token = Some(encrypted_device_token);
|
||||
self.run_command_and_check_ok(&format_setmetadata(
|
||||
&folder,
|
||||
&encrypted_device_token,
|
||||
))
|
||||
.await
|
||||
.context("SETMETADATA command failed")?;
|
||||
|
||||
// Store device token saved on the server
|
||||
// to prevent storing duplicate tokens.
|
||||
// The server cannot deduplicate on its own
|
||||
// because encryption gives a different
|
||||
// result each time.
|
||||
context
|
||||
.set_config_internal(Config::DeviceToken, Some(&device_token))
|
||||
.await?;
|
||||
} else {
|
||||
// If Apple or Google (FCM) gives us a very large token,
|
||||
// do not even try to give it to IMAP servers.
|
||||
@@ -1634,29 +1633,9 @@ impl Session {
|
||||
// of any length, but there is no reason for tokens
|
||||
// to be that large even after OpenPGP encryption.
|
||||
warn!(context, "Device token is too long for LITERAL-, ignoring.");
|
||||
new_encrypted_device_token = None;
|
||||
}
|
||||
} else {
|
||||
new_encrypted_device_token = old_encrypted_device_token;
|
||||
}
|
||||
|
||||
// Store new encrypted device token on the server
|
||||
// even if it is the same as the old one.
|
||||
if let Some(encrypted_device_token) = new_encrypted_device_token {
|
||||
let folder = context
|
||||
.get_config(Config::ConfiguredInboxFolder)
|
||||
.await?
|
||||
.context("INBOX is not configured")?;
|
||||
|
||||
self.run_command_and_check_ok(&format_setmetadata(
|
||||
&folder,
|
||||
&encrypted_device_token,
|
||||
))
|
||||
.await
|
||||
.context("SETMETADATA command failed")?;
|
||||
|
||||
context.push_subscribed.store(true, Ordering::Relaxed);
|
||||
}
|
||||
context.push_subscribed.store(true, Ordering::Relaxed);
|
||||
} else if !context.push_subscriber.heartbeat_subscribed().await {
|
||||
let context = context.clone();
|
||||
// Subscribe for heartbeat notifications.
|
||||
@@ -2641,4 +2620,343 @@ async fn add_all_recipients_as_contacts(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod imap_tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_get_folder_meaning_by_name() {
|
||||
assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent);
|
||||
assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent);
|
||||
assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("Messages envoyés"),
|
||||
FolderMeaning::Sent
|
||||
);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("mEsSaGes envoyÉs"),
|
||||
FolderMeaning::Sent
|
||||
);
|
||||
assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown);
|
||||
assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam);
|
||||
assert_eq!(get_folder_meaning_by_name("Trash"), FolderMeaning::Trash);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_uid_next_validity() {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
|
||||
set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap();
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7);
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
|
||||
set_uid_next(&t.ctx, "Inbox", 5).await.unwrap();
|
||||
set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap();
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_sequence_sets() {
|
||||
assert_eq!(build_sequence_sets(&[]).unwrap(), vec![]);
|
||||
|
||||
let cases = vec![
|
||||
(vec![1], "1"),
|
||||
(vec![3291], "3291"),
|
||||
(vec![1, 3, 5, 7, 9, 11], "1,3,5,7,9,11"),
|
||||
(vec![1, 2, 3], "1:3"),
|
||||
(vec![1, 4, 5, 6], "1,4:6"),
|
||||
((1..=500).collect(), "1:500"),
|
||||
(vec![3, 4, 8, 9, 10, 11, 39, 50, 2], "3:4,8:11,39,50,2"),
|
||||
];
|
||||
for (input, s) in cases {
|
||||
assert_eq!(
|
||||
build_sequence_sets(&input).unwrap(),
|
||||
vec![(input, s.into())]
|
||||
);
|
||||
}
|
||||
|
||||
let has_number = |(uids, s): &(Vec<u32>, String), number| {
|
||||
uids.iter().any(|&n| n == number)
|
||||
&& s.split(',').any(|n| n.parse::<u32>().unwrap() == number)
|
||||
};
|
||||
|
||||
let numbers: Vec<_> = (2..=500).step_by(2).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
assert!(result.len() == 1); // these UIDs fit in one set
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
|
||||
let numbers: Vec<_> = (1..=1000).step_by(3).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
let (last_uids, last_str) = result.last().unwrap();
|
||||
assert_eq!(
|
||||
last_uids.get((last_uids.len() - 2)..).unwrap(),
|
||||
&[997, 1000]
|
||||
);
|
||||
assert!(last_str.ends_with("997,1000"));
|
||||
assert!(result.len() == 2); // This time we need 2 sets
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
|
||||
let numbers: Vec<_> = (30000000..=30002500).step_by(4).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
assert_eq!(result.len(), 6);
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn check_target_folder_combination(
|
||||
folder: &str,
|
||||
mvbox_move: bool,
|
||||
chat_msg: bool,
|
||||
expected_destination: &str,
|
||||
accepted_chat: bool,
|
||||
outgoing: bool,
|
||||
setupmessage: bool,
|
||||
) -> Result<()> {
|
||||
println!("Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}");
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
|
||||
.await?;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredSentboxFolder, Some("Sent"))
|
||||
.await?;
|
||||
t.ctx
|
||||
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
|
||||
.await?;
|
||||
|
||||
if accepted_chat {
|
||||
let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?;
|
||||
ChatId::create_for_contact(&t.ctx, contact_id).await?;
|
||||
}
|
||||
let temp;
|
||||
|
||||
let bytes = if setupmessage {
|
||||
include_bytes!("../test-data/message/AutocryptSetupMessage.eml")
|
||||
} else {
|
||||
temp = format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
{}\
|
||||
Subject: foo\n\
|
||||
Message-ID: <abc@example.com>\n\
|
||||
{}\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
if outgoing {
|
||||
"From: alice@example.org\nTo: bob@example.net\n"
|
||||
} else {
|
||||
"From: bob@example.net\nTo: alice@example.org\n"
|
||||
},
|
||||
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
|
||||
);
|
||||
temp.as_bytes()
|
||||
};
|
||||
|
||||
let (headers, _) = mailparse::parse_headers(bytes)?;
|
||||
let actual = if let Some(config) =
|
||||
target_folder_cfg(&t, folder, get_folder_meaning_by_name(folder), &headers).await?
|
||||
{
|
||||
t.get_config(config).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let expected = if expected_destination == folder {
|
||||
None
|
||||
} else {
|
||||
Some(expected_destination)
|
||||
};
|
||||
assert_eq!(expected, actual.as_deref(), "For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// chat_msg means that the message was sent by Delta Chat
|
||||
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
|
||||
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", false, true, "INBOX"),
|
||||
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", true, true, "DeltaChat"),
|
||||
];
|
||||
|
||||
// These are the same as above, but non-chat messages in Spam stay in Spam
|
||||
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "Spam"),
|
||||
("Spam", false, true, "INBOX"),
|
||||
("Spam", true, false, "Spam"),
|
||||
("Spam", true, true, "DeltaChat"),
|
||||
];
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_incoming_accepted() -> Result<()> {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_incoming_request() -> Result<()> {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_outgoing() -> Result<()> {
|
||||
// Test outgoing emails
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_setupmsg() -> Result<()> {
|
||||
// Test setupmessages
|
||||
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_imap_search_command() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"FROM "alice@example.org""#
|
||||
);
|
||||
|
||||
t.ctx.set_primary_self_addr("alice@another.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
t.ctx.set_primary_self_addr("alice@third.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (OR (FROM "alice@third.com") (FROM "alice@another.com")) (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uid_grouper() {
|
||||
// Input: sequence of (rowid: i64, uid: u32, target: String)
|
||||
// Output: sequence of (target: String, rowid_set: Vec<i64>, uid_set: String)
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(res, vec![("INBOX".to_string(), vec![1], "2".to_string())]);
|
||||
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string()), (2, 3, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![("INBOX".to_string(), vec![1, 2], "2:3".to_string())]
|
||||
);
|
||||
|
||||
let grouper = UidGrouper::from([
|
||||
(1, 2, "INBOX".to_string()),
|
||||
(2, 2, "INBOX".to_string()),
|
||||
(3, 3, "INBOX".to_string()),
|
||||
]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_setmetadata_device_token() {
|
||||
assert_eq!(
|
||||
format_setmetadata("INBOX", "foobarbaz"),
|
||||
"SETMETADATA \"INBOX\" (/private/devicetoken {9+}\r\nfoobarbaz)"
|
||||
);
|
||||
assert_eq!(
|
||||
format_setmetadata("INBOX", "foo\r\nbar\r\nbaz\r\n"),
|
||||
"SETMETADATA \"INBOX\" (/private/devicetoken {15+}\r\nfoo\r\nbar\r\nbaz\r\n)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use async_channel::Receiver;
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use futures_lite::FutureExt;
|
||||
@@ -38,23 +38,18 @@ impl Session {
|
||||
}
|
||||
|
||||
if self.new_mail {
|
||||
info!(
|
||||
context,
|
||||
"Skipping IDLE in {folder:?} because there may be new mail."
|
||||
);
|
||||
return Ok(self);
|
||||
}
|
||||
|
||||
if let Ok(()) = idle_interrupt_receiver.try_recv() {
|
||||
info!(context, "Skip IDLE in {folder:?} because we got interrupt.");
|
||||
info!(context, "skip idle, got interrupt");
|
||||
return Ok(self);
|
||||
}
|
||||
|
||||
let mut handle = self.inner.idle();
|
||||
handle
|
||||
.init()
|
||||
.await
|
||||
.with_context(|| format!("IMAP IDLE protocol failed to init in folder {folder:?}"))?;
|
||||
if let Err(err) = handle.init().await {
|
||||
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
|
||||
}
|
||||
|
||||
// At this point IDLE command was sent and we received a "+ idling" response. We will now
|
||||
// read from the stream without getting any data for up to `IDLE_TIMEOUT`. If we don't
|
||||
@@ -68,10 +63,7 @@ impl Session {
|
||||
Interrupt,
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"IDLE entering wait-on-remote state in folder {folder:?}."
|
||||
);
|
||||
info!(context, "{folder}: Idle entering wait-on-remote state");
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
|
||||
idle_interrupt_receiver.recv().await.ok();
|
||||
|
||||
@@ -83,19 +75,19 @@ impl Session {
|
||||
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
|
||||
info!(context, "{folder:?}: Idle has NewData {x:?}");
|
||||
info!(context, "{folder}: Idle has NewData {:?}", x);
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||
info!(context, "{folder:?}: Idle-wait timeout or interruption.");
|
||||
info!(context, "{folder}: Idle-wait timeout or interruption");
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||
info!(context, "{folder:?}: Idle wait was interrupted manually.");
|
||||
info!(context, "{folder}: Idle wait was interrupted manually");
|
||||
}
|
||||
Ok(Event::Interrupt) => {
|
||||
info!(context, "{folder:?}: Idle wait was interrupted.");
|
||||
info!(context, "{folder}: Idle wait was interrupted");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "{folder:?}: Idle wait errored: {err:?}.");
|
||||
warn!(context, "{folder}: Idle wait errored: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,337 +0,0 @@
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_get_folder_meaning_by_name() {
|
||||
assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent);
|
||||
assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent);
|
||||
assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("Messages envoyés"),
|
||||
FolderMeaning::Sent
|
||||
);
|
||||
assert_eq!(
|
||||
get_folder_meaning_by_name("mEsSaGes envoyÉs"),
|
||||
FolderMeaning::Sent
|
||||
);
|
||||
assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown);
|
||||
assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam);
|
||||
assert_eq!(get_folder_meaning_by_name("Trash"), FolderMeaning::Trash);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_uid_next_validity() {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
|
||||
set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap();
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7);
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
|
||||
|
||||
set_uid_next(&t.ctx, "Inbox", 5).await.unwrap();
|
||||
set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap();
|
||||
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5);
|
||||
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_sequence_sets() {
|
||||
assert_eq!(build_sequence_sets(&[]).unwrap(), vec![]);
|
||||
|
||||
let cases = vec![
|
||||
(vec![1], "1"),
|
||||
(vec![3291], "3291"),
|
||||
(vec![1, 3, 5, 7, 9, 11], "1,3,5,7,9,11"),
|
||||
(vec![1, 2, 3], "1:3"),
|
||||
(vec![1, 4, 5, 6], "1,4:6"),
|
||||
((1..=500).collect(), "1:500"),
|
||||
(vec![3, 4, 8, 9, 10, 11, 39, 50, 2], "3:4,8:11,39,50,2"),
|
||||
];
|
||||
for (input, s) in cases {
|
||||
assert_eq!(
|
||||
build_sequence_sets(&input).unwrap(),
|
||||
vec![(input, s.into())]
|
||||
);
|
||||
}
|
||||
|
||||
let has_number = |(uids, s): &(Vec<u32>, String), number| {
|
||||
uids.iter().any(|&n| n == number)
|
||||
&& s.split(',').any(|n| n.parse::<u32>().unwrap() == number)
|
||||
};
|
||||
|
||||
let numbers: Vec<_> = (2..=500).step_by(2).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
assert!(result.len() == 1); // these UIDs fit in one set
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
|
||||
let numbers: Vec<_> = (1..=1000).step_by(3).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
let (last_uids, last_str) = result.last().unwrap();
|
||||
assert_eq!(
|
||||
last_uids.get((last_uids.len() - 2)..).unwrap(),
|
||||
&[997, 1000]
|
||||
);
|
||||
assert!(last_str.ends_with("997,1000"));
|
||||
assert!(result.len() == 2); // This time we need 2 sets
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
|
||||
let numbers: Vec<_> = (30000000..=30002500).step_by(4).collect();
|
||||
let result = build_sequence_sets(&numbers).unwrap();
|
||||
for (_, set) in &result {
|
||||
assert!(set.len() < 1010);
|
||||
assert!(!set.ends_with(','));
|
||||
assert!(!set.starts_with(','));
|
||||
}
|
||||
assert_eq!(result.len(), 6);
|
||||
for &number in &numbers {
|
||||
assert!(result.iter().any(|r| has_number(r, number)));
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_target_folder_combination(
|
||||
folder: &str,
|
||||
mvbox_move: bool,
|
||||
chat_msg: bool,
|
||||
expected_destination: &str,
|
||||
accepted_chat: bool,
|
||||
outgoing: bool,
|
||||
setupmessage: bool,
|
||||
) -> Result<()> {
|
||||
println!("Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}");
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
|
||||
.await?;
|
||||
t.ctx
|
||||
.set_config(Config::ConfiguredSentboxFolder, Some("Sent"))
|
||||
.await?;
|
||||
t.ctx
|
||||
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
|
||||
.await?;
|
||||
|
||||
if accepted_chat {
|
||||
let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?;
|
||||
ChatId::create_for_contact(&t.ctx, contact_id).await?;
|
||||
}
|
||||
let temp;
|
||||
|
||||
let bytes = if setupmessage {
|
||||
include_bytes!("../../test-data/message/AutocryptSetupMessage.eml")
|
||||
} else {
|
||||
temp = format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
{}\
|
||||
Subject: foo\n\
|
||||
Message-ID: <abc@example.com>\n\
|
||||
{}\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
if outgoing {
|
||||
"From: alice@example.org\nTo: bob@example.net\n"
|
||||
} else {
|
||||
"From: bob@example.net\nTo: alice@example.org\n"
|
||||
},
|
||||
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
|
||||
);
|
||||
temp.as_bytes()
|
||||
};
|
||||
|
||||
let (headers, _) = mailparse::parse_headers(bytes)?;
|
||||
let actual = if let Some(config) =
|
||||
target_folder_cfg(&t, folder, get_folder_meaning_by_name(folder), &headers).await?
|
||||
{
|
||||
t.get_config(config).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let expected = if expected_destination == folder {
|
||||
None
|
||||
} else {
|
||||
Some(expected_destination)
|
||||
};
|
||||
assert_eq!(expected, actual.as_deref(), "For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// chat_msg means that the message was sent by Delta Chat
|
||||
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
|
||||
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", false, true, "INBOX"),
|
||||
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", true, true, "DeltaChat"),
|
||||
];
|
||||
|
||||
// These are the same as above, but non-chat messages in Spam stay in Spam
|
||||
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Sent", false, false, "Sent"),
|
||||
("Sent", false, true, "Sent"),
|
||||
("Sent", true, false, "Sent"),
|
||||
("Sent", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "Spam"),
|
||||
("Spam", false, true, "INBOX"),
|
||||
("Spam", true, false, "Spam"),
|
||||
("Spam", true, true, "DeltaChat"),
|
||||
];
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_incoming_accepted() -> Result<()> {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_incoming_request() -> Result<()> {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_outgoing() -> Result<()> {
|
||||
// Test outgoing emails
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
expected_destination,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_setupmsg() -> Result<()> {
|
||||
// Test setupmessages
|
||||
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_imap_search_command() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"FROM "alice@example.org""#
|
||||
);
|
||||
|
||||
t.ctx.set_primary_self_addr("alice@another.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (FROM "alice@another.com") (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
t.ctx.set_primary_self_addr("alice@third.com").await?;
|
||||
assert_eq!(
|
||||
get_imap_self_sent_search_command(&t.ctx).await?,
|
||||
r#"OR (OR (FROM "alice@third.com") (FROM "alice@another.com")) (FROM "alice@example.org")"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uid_grouper() {
|
||||
// Input: sequence of (rowid: i64, uid: u32, target: String)
|
||||
// Output: sequence of (target: String, rowid_set: Vec<i64>, uid_set: String)
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(res, vec![("INBOX".to_string(), vec![1], "2".to_string())]);
|
||||
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string()), (2, 3, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![("INBOX".to_string(), vec![1, 2], "2:3".to_string())]
|
||||
);
|
||||
|
||||
let grouper = UidGrouper::from([
|
||||
(1, 2, "INBOX".to_string()),
|
||||
(2, 2, "INBOX".to_string()),
|
||||
(3, 3, "INBOX".to_string()),
|
||||
]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_setmetadata_device_token() {
|
||||
assert_eq!(
|
||||
format_setmetadata("INBOX", "foobarbaz"),
|
||||
"SETMETADATA \"INBOX\" (/private/devicetoken {9+}\r\nfoobarbaz)"
|
||||
);
|
||||
assert_eq!(
|
||||
format_setmetadata("INBOX", "foo\r\nbar\r\nbaz\r\n"),
|
||||
"SETMETADATA \"INBOX\" (/private/devicetoken {15+}\r\nfoo\r\nbar\r\nbaz\r\n)"
|
||||
);
|
||||
}
|
||||
@@ -26,11 +26,12 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
/* this may require a keypair to be created. this may take a second ... */
|
||||
let setup_file_content = render_setup_file(context, &setup_code).await?;
|
||||
/* encrypting may also take a while ... */
|
||||
let setup_file_blob = BlobObject::create_and_deduplicate_from_bytes(
|
||||
let setup_file_blob = BlobObject::create(
|
||||
context,
|
||||
setup_file_content.as_bytes(),
|
||||
"autocrypt-setup-message.html",
|
||||
)?;
|
||||
setup_file_content.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
|
||||
let mut msg = Message {
|
||||
@@ -38,8 +39,6 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set(Param::File, setup_file_blob.as_name());
|
||||
msg.param
|
||||
.set(Param::Filename, "autocrypt-setup-message.html");
|
||||
msg.subject = stock_str::ac_setup_msg_subject(context).await;
|
||||
msg.param
|
||||
.set(Param::MimeType, "application/autocrypt-setup");
|
||||
|
||||
@@ -33,7 +33,8 @@ use std::task::Poll;
|
||||
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use futures_lite::FutureExt;
|
||||
use iroh::{Endpoint, RelayMode};
|
||||
use iroh_net::relay::RelayMode;
|
||||
use iroh_net::Endpoint;
|
||||
use tokio::fs;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -64,11 +65,11 @@ const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
|
||||
/// task use the [`Context::stop_ongoing`] mechanism.
|
||||
#[derive(Debug)]
|
||||
pub struct BackupProvider {
|
||||
/// iroh endpoint.
|
||||
/// iroh-net endpoint.
|
||||
_endpoint: Endpoint,
|
||||
|
||||
/// iroh address.
|
||||
node_addr: iroh::NodeAddr,
|
||||
/// iroh-net address.
|
||||
node_addr: iroh_net::NodeAddr,
|
||||
|
||||
/// Authentication token that should be submitted
|
||||
/// to retrieve the backup.
|
||||
@@ -161,7 +162,7 @@ impl BackupProvider {
|
||||
|
||||
async fn handle_connection(
|
||||
context: Context,
|
||||
conn: iroh::endpoint::Connecting,
|
||||
conn: iroh_net::endpoint::Connecting,
|
||||
auth_token: String,
|
||||
dbfile: Arc<TempPathGuard>,
|
||||
) -> Result<()> {
|
||||
@@ -291,7 +292,7 @@ impl Future for BackupProvider {
|
||||
|
||||
pub async fn get_backup2(
|
||||
context: &Context,
|
||||
node_addr: iroh::NodeAddr,
|
||||
node_addr: iroh_net::NodeAddr,
|
||||
auth_token: String,
|
||||
) -> Result<()> {
|
||||
let relay_mode = RelayMode::Disabled;
|
||||
@@ -341,7 +342,7 @@ pub async fn get_backup2(
|
||||
/// This is a long running operation which will return only when completed.
|
||||
///
|
||||
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variant of it. It
|
||||
/// does avoid having [`iroh::NodeAddr`] in the primary API however, without
|
||||
/// does avoid having [`iroh_net::NodeAddr`] in the primary API however, without
|
||||
/// having to revert to untyped bytes.
|
||||
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
match qr {
|
||||
@@ -393,8 +394,7 @@ mod tests {
|
||||
let file = ctx0.get_blobdir().join("hello.txt");
|
||||
fs::write(&file, "i am attachment").await.unwrap();
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_and_deduplicate(&ctx0, &file, Some("hello.txt"), Some("text/plain"))
|
||||
.unwrap();
|
||||
msg.set_file(file.to_str().unwrap(), Some("text/plain"));
|
||||
send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
|
||||
|
||||
// Prepare to transfer backup.
|
||||
@@ -428,12 +428,7 @@ mod tests {
|
||||
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
|
||||
|
||||
let path = msg.get_file(&ctx1).unwrap();
|
||||
assert_eq!(
|
||||
// That's the hash of the file:
|
||||
path.with_file_name("ac1d2d284757656a8d41dc40aae4136.txt"),
|
||||
path
|
||||
);
|
||||
assert_eq!("hello.txt", msg.get_filename().unwrap());
|
||||
assert_eq!(path.with_file_name("hello.txt"), path);
|
||||
let text = fs::read_to_string(&path).await.unwrap();
|
||||
assert_eq!(text, "i am attachment");
|
||||
|
||||
|
||||
@@ -1074,7 +1074,7 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
let file = alice.get_blobdir().join(file_name);
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some("logo.png"), None)?;
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let sent = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let alice_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
|
||||
assert_eq!(alice_msg.has_location(), false);
|
||||
|
||||
901
src/message.rs
901
src/message.rs
@@ -470,7 +470,6 @@ pub struct Message {
|
||||
/// `In-Reply-To` header value.
|
||||
pub(crate) in_reply_to: Option<String>,
|
||||
pub(crate) is_dc_message: MessengerMessage,
|
||||
pub(crate) original_msg_id: MsgId,
|
||||
pub(crate) mime_modified: bool,
|
||||
pub(crate) chat_blocked: Blocked,
|
||||
pub(crate) location_id: u32,
|
||||
@@ -537,7 +536,6 @@ impl Message {
|
||||
" m.download_state AS download_state,",
|
||||
" m.error AS error,",
|
||||
" m.msgrmsg AS msgrmsg,",
|
||||
" m.starred AS original_msg_id,",
|
||||
" m.mime_modified AS mime_modified,",
|
||||
" m.txt AS txt,",
|
||||
" m.subject AS subject,",
|
||||
@@ -594,7 +592,6 @@ impl Message {
|
||||
error: Some(row.get::<_, String>("error")?)
|
||||
.filter(|error| !error.is_empty()),
|
||||
is_dc_message: row.get("msgrmsg")?,
|
||||
original_msg_id: row.get("original_msg_id")?,
|
||||
mime_modified: row.get("mime_modified")?,
|
||||
text,
|
||||
subject: row.get("subject")?,
|
||||
@@ -623,8 +620,8 @@ impl Message {
|
||||
pub fn get_filemime(&self) -> Option<String> {
|
||||
if let Some(m) = self.param.get(Param::MimeType) {
|
||||
return Some(m.to_string());
|
||||
} else if self.param.exists(Param::File) {
|
||||
if let Some((_, mime)) = guess_msgtype_from_suffix(self) {
|
||||
} else if let Some(file) = self.param.get(Param::File) {
|
||||
if let Some((_, mime)) = guess_msgtype_from_suffix(Path::new(file)) {
|
||||
return Some(mime.to_string());
|
||||
}
|
||||
// we have a file but no mimetype, let's use a generic one
|
||||
@@ -1085,62 +1082,18 @@ impl Message {
|
||||
self.param.set_optional(Param::MimeType, filemime);
|
||||
}
|
||||
|
||||
/// Sets the file associated with a message, deduplicating files with the same name.
|
||||
///
|
||||
/// If `name` is Some, it is used as the file name
|
||||
/// and the actual current name of the file is ignored.
|
||||
///
|
||||
/// If the source file is already in the blobdir, it will be renamed,
|
||||
/// otherwise it will be copied to the blobdir first.
|
||||
///
|
||||
/// 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.
|
||||
pub fn set_file_and_deduplicate(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
file: &Path,
|
||||
name: Option<&str>,
|
||||
filemime: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let name = if let Some(name) = name {
|
||||
name.to_string()
|
||||
} else {
|
||||
file.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "unknown_file".to_string())
|
||||
};
|
||||
|
||||
let blob = BlobObject::create_and_deduplicate(context, file, Path::new(&name))?;
|
||||
self.param.set(Param::File, blob.as_name());
|
||||
|
||||
self.param.set(Param::Filename, name);
|
||||
self.param.set_optional(Param::MimeType, filemime);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a new blob and sets it as a file associated with a message.
|
||||
///
|
||||
/// In order to deduplicate files that contain the same data,
|
||||
/// the file will be named `<hash>.<extension>`, e.g. `ce940175885d7b78f7b7e9f1396611f.jpg`.
|
||||
///
|
||||
/// NOTE: The file must not be modified after this function was called.
|
||||
pub fn set_file_from_bytes(
|
||||
pub async fn set_file_from_bytes(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
name: &str,
|
||||
suggested_name: &str,
|
||||
data: &[u8],
|
||||
filemime: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(context, data, name)?;
|
||||
self.param.set(Param::Filename, name);
|
||||
let blob = BlobObject::create(context, suggested_name, data).await?;
|
||||
self.param.set(Param::Filename, suggested_name);
|
||||
self.param.set(Param::File, blob.as_name());
|
||||
self.param.set_optional(Param::MimeType, filemime);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1153,6 +1106,7 @@ impl Message {
|
||||
);
|
||||
let vcard = contact::make_vcard(context, contacts).await?;
|
||||
self.set_file_from_bytes(context, "vcard.vcf", vcard.as_bytes(), None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Updates message state from the vCard attachment.
|
||||
@@ -1302,35 +1256,6 @@ impl Message {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns original message ID for message from "Saved Messages".
|
||||
pub async fn get_original_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
|
||||
if !self.original_msg_id.is_special() {
|
||||
if let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
|
||||
{
|
||||
return if msg.chat_id.is_trash() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(msg.id))
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Check if the message was saved and returns the corresponding message inside "Saved Messages".
|
||||
/// UI can use this to show a symbol beside the message, indicating it was saved.
|
||||
/// The message can be un-saved by deleting the returned message.
|
||||
pub async fn get_saved_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
|
||||
let res: Option<MsgId> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id FROM msgs WHERE starred=? AND chat_id!=?",
|
||||
(self.id, DC_CHAT_ID_TRASH),
|
||||
)
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Force the message to be sent in plain text.
|
||||
pub fn force_plaintext(&mut self) {
|
||||
self.param.set_int(Param::ForcePlaintext, 1);
|
||||
@@ -1510,14 +1435,7 @@ pub async fn get_msg_read_receipts(
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) fn guess_msgtype_from_suffix(msg: &Message) -> Option<(Viewtype, &'static str)> {
|
||||
msg.param
|
||||
.get(Param::Filename)
|
||||
.or_else(|| msg.param.get(Param::File))
|
||||
.and_then(|file| guess_msgtype_from_path_suffix(Path::new(file)))
|
||||
}
|
||||
|
||||
pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &'static str)> {
|
||||
pub(crate) fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
||||
let extension: &str = &path.extension()?.to_str()?.to_lowercase();
|
||||
let info = match extension {
|
||||
// before using viewtype other than Viewtype::File,
|
||||
@@ -1751,12 +1669,12 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
.set_config_internal(Config::LastMsgId, Some(&last_msg_id.to_u32().to_string()))
|
||||
.await?;
|
||||
|
||||
let mut msgs = Vec::with_capacity(msg_ids.len());
|
||||
for &id in &msg_ids {
|
||||
if let Some(msg) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
let msgs = context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
"SELECT
|
||||
m.id AS id,
|
||||
m.chat_id AS chat_id,
|
||||
m.state AS state,
|
||||
m.download_state as download_state,
|
||||
@@ -1767,39 +1685,39 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
c.archived AS archived,
|
||||
c.blocked AS blocked
|
||||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
|
||||
WHERE m.id=? AND m.chat_id>9",
|
||||
(id,),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let state: MessageState = row.get("state")?;
|
||||
let download_state: DownloadState = row.get("download_state")?;
|
||||
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
|
||||
let from_id: ContactId = row.get("from_id")?;
|
||||
let rfc724_mid: String = row.get("rfc724_mid")?;
|
||||
let visibility: ChatVisibility = row.get("archived")?;
|
||||
let blocked: Option<Blocked> = row.get("blocked")?;
|
||||
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
|
||||
Ok((
|
||||
(
|
||||
id,
|
||||
chat_id,
|
||||
state,
|
||||
download_state,
|
||||
param,
|
||||
from_id,
|
||||
rfc724_mid,
|
||||
visibility,
|
||||
blocked.unwrap_or_default(),
|
||||
),
|
||||
ephemeral_timer,
|
||||
))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
msgs.push(msg);
|
||||
}
|
||||
}
|
||||
WHERE m.id IN ({}) AND m.chat_id>9",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(&msg_ids),
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let state: MessageState = row.get("state")?;
|
||||
let download_state: DownloadState = row.get("download_state")?;
|
||||
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
|
||||
let from_id: ContactId = row.get("from_id")?;
|
||||
let rfc724_mid: String = row.get("rfc724_mid")?;
|
||||
let visibility: ChatVisibility = row.get("archived")?;
|
||||
let blocked: Option<Blocked> = row.get("blocked")?;
|
||||
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
|
||||
Ok((
|
||||
(
|
||||
id,
|
||||
chat_id,
|
||||
state,
|
||||
download_state,
|
||||
param,
|
||||
from_id,
|
||||
rfc724_mid,
|
||||
visibility,
|
||||
blocked.unwrap_or_default(),
|
||||
),
|
||||
ephemeral_timer,
|
||||
))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if msgs
|
||||
.iter()
|
||||
@@ -2245,4 +2163,729 @@ pub(crate) fn normalize_text(text: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod message_tests;
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
self, add_contact_to_chat, marknoticed_chat, send_text_msg, ChatItem, ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::reaction::send_reaction;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils as test;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_guess_msgtype_from_suffix() {
|
||||
assert_eq!(
|
||||
guess_msgtype_from_suffix(Path::new("foo/bar-sth.mp3")),
|
||||
Some((Viewtype::Audio, "audio/mpeg"))
|
||||
);
|
||||
assert_eq!(
|
||||
guess_msgtype_from_suffix(Path::new("foo/file.html")),
|
||||
Some((Viewtype::File, "text/html"))
|
||||
);
|
||||
assert_eq!(
|
||||
guess_msgtype_from_suffix(Path::new("foo/file.xdc")),
|
||||
Some((Viewtype::Webxdc, "application/webxdc+zip"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webrtc_instance() {
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
|
||||
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
|
||||
assert_eq!(url, "https://foo/bar");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("bAsIcwEbrTc:url");
|
||||
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
|
||||
assert_eq!(url, "url");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("https://foo/bar?key=val#key=val");
|
||||
assert_eq!(webrtc_type, VideochatType::Unknown);
|
||||
assert_eq!(url, "https://foo/bar?key=val#key=val");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("jitsi:https://j.si/foo");
|
||||
assert_eq!(webrtc_type, VideochatType::Jitsi);
|
||||
assert_eq!(url, "https://j.si/foo");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_webrtc_instance() {
|
||||
// webrtc_instance may come from an input field of the ui, be pretty tolerant on input
|
||||
let instance = Message::create_webrtc_instance("https://meet.jit.si/", "123");
|
||||
assert_eq!(instance, "https://meet.jit.si/123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("https://meet.jit.si", "456");
|
||||
assert_eq!(instance, "https://meet.jit.si/456");
|
||||
|
||||
let instance = Message::create_webrtc_instance("meet.jit.si", "789");
|
||||
assert_eq!(instance, "https://meet.jit.si/789");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo?", "123");
|
||||
assert_eq!(instance, "https://bla.foo?123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("jitsi:bla.foo#", "456");
|
||||
assert_eq!(instance, "jitsi:https://bla.foo#456");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room=", "789");
|
||||
assert_eq!(instance, "https://bla.foo#room=789");
|
||||
|
||||
let instance = Message::create_webrtc_instance("https://bla.foo#room", "123");
|
||||
assert_eq!(instance, "https://bla.foo#room/123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room$ROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo#room123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room=$ROOM&after=cont", "234");
|
||||
assert_eq!(instance, "https://bla.foo#room=234&after=cont");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" meet.jit .si ", "789");
|
||||
assert_eq!(instance, "https://meet.jit.si/789");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" basicwebrtc: basic . stuff\n ", "12345ab");
|
||||
assert_eq!(instance, "basicwebrtc:https://basic.stuff/12345ab");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_webrtc_instance_noroom() {
|
||||
// webrtc_instance may come from an input field of the ui, be pretty tolerant on input
|
||||
let instance = Message::create_webrtc_instance("bla.foo$NOROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" bla . foo $NOROOM ", "456");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" $NOROOM bla . foo ", "789");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" bla.foo / $NOROOM ? a = b ", "123");
|
||||
assert_eq!(instance, "https://bla.foo/?a=b");
|
||||
|
||||
// $ROOM has a higher precedence
|
||||
let instance = Message::create_webrtc_instance("bla.foo/?$NOROOM=$ROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo/?$NOROOM=123");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_width_height() {
|
||||
let t = test::TestContext::new().await;
|
||||
|
||||
// test that get_width() and get_height() are returning some dimensions for images;
|
||||
// (as the device-chat contains a welcome-images, we check that)
|
||||
t.update_device_chats().await.ok();
|
||||
let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut has_image = false;
|
||||
let chatitems = chat::get_chat_msgs(&t, device_chat_id).await.unwrap();
|
||||
for chatitem in chatitems {
|
||||
if let ChatItem::Message { msg_id } = chatitem {
|
||||
if let Ok(msg) = Message::load_from_db(&t, msg_id).await {
|
||||
if msg.get_viewtype() == Viewtype::Image {
|
||||
has_image = true;
|
||||
// just check that width/height are inside some reasonable ranges
|
||||
assert!(msg.get_width() > 100);
|
||||
assert!(msg.get_height() > 100);
|
||||
assert!(msg.get_width() < 4000);
|
||||
assert!(msg.get_height() < 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(has_image);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quote() {
|
||||
let d = test::TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
ctx.set_config(Config::ConfiguredAddr, Some("self@example.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat = d.create_chat_with_contact("", "dest@example.com").await;
|
||||
|
||||
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());
|
||||
|
||||
let mut msg2 = Message::new(Viewtype::Text);
|
||||
msg2.set_quote(ctx, Some(&msg))
|
||||
.await
|
||||
.expect("can't set quote");
|
||||
assert_eq!(msg2.quoted_text().unwrap(), msg.get_text());
|
||||
|
||||
let quoted_msg = msg2
|
||||
.quoted_message(ctx)
|
||||
.await
|
||||
.expect("error while retrieving quoted message")
|
||||
.expect("quoted message not found");
|
||||
assert_eq!(quoted_msg.get_text(), msg2.quoted_text().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_quote() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.send_recv_accept(alice, bob, "Hi!").await;
|
||||
let msg = tcm
|
||||
.send_recv(
|
||||
alice,
|
||||
bob,
|
||||
"On 2024-08-28, Alice wrote:\n> A quote.\nNot really.",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(msg.quoted_text().is_none());
|
||||
assert!(msg.quoted_message(bob).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unencrypted_quote_encrypted_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_group = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "Group chat", &[bob])
|
||||
.await;
|
||||
let sent = alice.send_text(alice_group, "Hi! I created a group").await;
|
||||
let bob_received_message = bob.recv_msg(&sent).await;
|
||||
|
||||
let bob_group = bob_received_message.chat_id;
|
||||
bob_group.accept(bob).await?;
|
||||
let sent = bob.send_text(bob_group, "Encrypted message").await;
|
||||
let alice_received_message = alice.recv_msg(&sent).await;
|
||||
assert!(alice_received_message.get_showpadlock());
|
||||
|
||||
// Alice adds contact without key so chat becomes unencrypted.
|
||||
let alice_flubby_contact_id =
|
||||
Contact::create(alice, "Flubby", "flubby@example.org").await?;
|
||||
add_contact_to_chat(alice, alice_group, alice_flubby_contact_id).await?;
|
||||
|
||||
// Alice quotes encrypted message in unencrypted chat.
|
||||
let mut msg = Message::new_text("unencrypted".to_string());
|
||||
msg.set_quote(alice, Some(&alice_received_message)).await?;
|
||||
chat::send_msg(alice, alice_group, &mut msg).await?;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(bob_received_message.quoted_text().unwrap(), "...");
|
||||
assert_eq!(bob_received_message.get_showpadlock(), false);
|
||||
|
||||
// Alice replaces a quote of encrypted message with a quote of unencrypted one.
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.set_quote(alice, Some(&alice_received_message)).await?;
|
||||
msg1.set_quote(alice, Some(&msg)).await?;
|
||||
chat::send_msg(alice, alice_group, &mut msg1).await?;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(bob_received_message.quoted_text().unwrap(), "unencrypted");
|
||||
assert_eq!(bob_received_message.get_showpadlock(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_chat_id() {
|
||||
// Alice receives a message that pops up as a contact request
|
||||
let alice = TestContext::new_alice().await;
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <123@example.com>\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// check chat-id of this message
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert!(!msg.get_chat_id().is_special());
|
||||
assert_eq!(msg.get_text(), "hello".to_string());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_override_sender_name() {
|
||||
// send message with overridden sender name
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let contact_id = *chat::get_chat_contacts(&alice, chat.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await.unwrap();
|
||||
|
||||
let mut msg = Message::new_text("bla blubb".to_string());
|
||||
msg.set_override_sender_name(Some("over ride".to_string()));
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
Some("over ride".to_string())
|
||||
);
|
||||
assert_eq!(msg.get_sender_name(&contact), "over ride".to_string());
|
||||
assert_ne!(contact.get_display_name(), "over ride".to_string());
|
||||
chat::send_msg(&alice, chat.id, &mut msg).await.unwrap();
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// bob receives that message
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let contact_id = *chat::get_chat_contacts(&bob, chat.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&bob, contact_id).await.unwrap();
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
assert_eq!(msg.text, "bla blubb");
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
Some("over ride".to_string())
|
||||
);
|
||||
assert_eq!(msg.get_sender_name(&contact), "over ride".to_string());
|
||||
assert_ne!(contact.get_display_name(), "over ride".to_string());
|
||||
|
||||
// explicitly check that the message does not create a mailing list
|
||||
// (mailing lists may also use `Sender:`-header)
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await.unwrap();
|
||||
assert_ne!(chat.typ, Chattype::Mailinglist);
|
||||
|
||||
// Alice receives message on another device.
|
||||
let msg = alice2.recv_msg(&sent_msg).await;
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
Some("over ride".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_markseen_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
|
||||
// alice sends to bob,
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
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 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);
|
||||
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
// that has no effect in contact request
|
||||
markseen_msgs(&bob, vec![msg1.id, msg2.id]).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
let bob_chat = Chat::load_from_db(&bob, bob_chat_id).await?;
|
||||
assert_eq!(bob_chat.blocked, Blocked::Request);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
bob_chat_id.accept(&bob).await.unwrap();
|
||||
|
||||
// bob sends to alice,
|
||||
// alice knows bob and messages appear in normal chat
|
||||
let msg1 = alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
let msg2 = alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0)?, alice_chat.id);
|
||||
assert_eq!(chats.get_chat_id(0)?, msg1.chat_id);
|
||||
assert_eq!(chats.get_chat_id(0)?, msg2.chat_id);
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 2);
|
||||
|
||||
// no message-ids, that should have no effect
|
||||
markseen_msgs(&alice, vec![]).await?;
|
||||
|
||||
// bad message-id, that should have no effect
|
||||
markseen_msgs(&alice, vec![MsgId::new(123456)]).await?;
|
||||
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 2);
|
||||
|
||||
// mark the most recent as seen
|
||||
markseen_msgs(&alice, vec![msg2.id]).await?;
|
||||
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 1);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 1);
|
||||
|
||||
// user scrolled up - mark both as seen
|
||||
markseen_msgs(&alice, vec![msg1.id, msg2.id]).await?;
|
||||
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 0);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_markseen_not_downloaded_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
|
||||
let file_bytes = include_bytes!("../test-data/image/screenshot.png");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)
|
||||
.await?;
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
// A not downloaded message can be seen only if it's seen on another device.
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
// Marking the message as seen again is a no op.
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::InProgress)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::Failure)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::Undecipherable)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
|
||||
assert!(
|
||||
!alice
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?
|
||||
);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
// Let's assume that Alice and Bob resolved the problem with encryption.
|
||||
let old_msg = msg;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, old_msg.chat_id);
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert!(msg.get_showpadlock());
|
||||
// The message state mustn't be downgraded to `InFresh`.
|
||||
assert_eq!(msg.state, MessageState::InNoticed);
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
let msg = Message::load_from_db(alice, msg.id).await?;
|
||||
assert_eq!(msg.state, MessageState::InSeen);
|
||||
assert_eq!(
|
||||
alice
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
|
||||
let file_bytes = include_bytes!("../test-data/image/screenshot.png");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)
|
||||
.await?;
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
let seen = true;
|
||||
let rcvd_msg = receive_imf(alice, sent_msg.payload().as_bytes(), seen)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(rcvd_msg.chat_id, msg.chat_id);
|
||||
let msg = Message::load_from_db(alice, *rcvd_msg.msg_ids.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert!(msg.get_showpadlock());
|
||||
assert_eq!(msg.state, MessageState::InSeen);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_state() -> Result<()> {
|
||||
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;
|
||||
|
||||
// check both get_state() functions,
|
||||
// the one requiring a id and the one requiring an object
|
||||
async fn assert_state(t: &Context, msg_id: MsgId, state: MessageState) {
|
||||
assert_eq!(msg_id.get_state(t).await.unwrap(), state);
|
||||
assert_eq!(
|
||||
Message::load_from_db(t, msg_id).await.unwrap().get_state(),
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
// check outgoing messages states on sender side
|
||||
let mut alice_msg = Message::new_text("hi!".to_string());
|
||||
assert_eq!(alice_msg.get_state(), MessageState::Undefined); // message not yet in db, assert_state() won't work
|
||||
|
||||
alice_chat
|
||||
.id
|
||||
.set_draft(&alice, Some(&mut alice_msg))
|
||||
.await?;
|
||||
let mut alice_msg = alice_chat.id.get_draft(&alice).await?.unwrap();
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutDraft).await;
|
||||
|
||||
let msg_id = chat::send_msg(&alice, alice_chat.id, &mut alice_msg).await?;
|
||||
assert_eq!(msg_id, alice_msg.id);
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutPending).await;
|
||||
|
||||
let payload = alice.pop_sent_msg().await;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutDelivered).await;
|
||||
|
||||
set_msg_failed(&alice, &mut alice_msg, "badly failed").await?;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutFailed).await;
|
||||
|
||||
// check incoming message states on receiver side
|
||||
let bob_msg = bob.recv_msg(&payload).await;
|
||||
assert_eq!(bob_chat.id, bob_msg.chat_id);
|
||||
assert_state(&bob, bob_msg.id, MessageState::InFresh).await;
|
||||
|
||||
marknoticed_chat(&bob, bob_msg.chat_id).await?;
|
||||
assert_state(&bob, bob_msg.id, MessageState::InNoticed).await;
|
||||
|
||||
markseen_msgs(&bob, vec![bob_msg.id]).await?;
|
||||
assert_state(&bob, bob_msg.id, MessageState::InSeen).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_is_bot() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Alice receives an auto-generated non-chat message.
|
||||
//
|
||||
// This could be a holiday notice,
|
||||
// in which case the message should be marked as bot-generated,
|
||||
// but the contact should not.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Claire <claire@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <789@example.com>\n\
|
||||
Auto-Submitted: auto-generated\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), "hello".to_string());
|
||||
assert!(msg.is_bot());
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert!(!contact.is_bot());
|
||||
|
||||
// Alice receives a message from Bob the bot.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <123@example.com>\n\
|
||||
Auto-Submitted: auto-generated\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), "hello".to_string());
|
||||
assert!(msg.is_bot());
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert!(contact.is_bot());
|
||||
|
||||
// Alice receives a message from Bob who is not the bot anymore.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <456@example.com>\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello again\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), "hello again".to_string());
|
||||
assert!(!msg.is_bot());
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert!(!contact.is_bot());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_viewtype_derive_display_works_as_expected() {
|
||||
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_viewtype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::default());
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
|
||||
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
|
||||
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
|
||||
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
|
||||
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
|
||||
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
|
||||
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
|
||||
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
|
||||
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
|
||||
assert_eq!(
|
||||
Viewtype::VideochatInvitation,
|
||||
Viewtype::from_i32(70).unwrap()
|
||||
);
|
||||
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
|
||||
assert_eq!(Viewtype::Vcard, Viewtype::from_i32(90).unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_quotes() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
|
||||
let sent = alice.send_text(chat.id, "> First quote").await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, "> First quote");
|
||||
assert!(received.quoted_text().is_none());
|
||||
assert!(received.quoted_message(&bob).await?.is_none());
|
||||
|
||||
let sent = alice.send_text(chat.id, "> Second quote").await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, "> Second quote");
|
||||
assert!(received.quoted_text().is_none());
|
||||
assert!(received.quoted_message(&bob).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_message_summary_text() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.get_self_chat().await;
|
||||
let msg_id = send_text_msg(&t, chat.id, "foo".to_string()).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
let summary = msg.get_summary(&t, None).await?;
|
||||
assert_eq!(summary.text, "foo");
|
||||
|
||||
// message summary does not change when reactions are applied (in contrast to chatlist summary)
|
||||
send_reaction(&t, msg_id, "🫵").await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
let summary = msg.get_summary(&t, None).await?;
|
||||
assert_eq!(summary.text, "foo");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_format_flowed_round_trip() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
|
||||
let text = " Foo bar";
|
||||
let sent = alice.send_text(chat.id, text).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, text);
|
||||
|
||||
let text = "Foo bar baz";
|
||||
let sent = alice.send_text(chat.id, text).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, text);
|
||||
|
||||
let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
|
||||
let sent = alice.send_text(chat.id, text).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, text);
|
||||
|
||||
let python_program = "\
|
||||
def hello():
|
||||
return 'Hello, world!'";
|
||||
let sent = alice.send_text(chat.id, python_program).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, python_program);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_msgs_offline() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
delete_msgs(&alice, &[msg.id]).await?;
|
||||
assert!(!alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,757 +0,0 @@
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
self, add_contact_to_chat, forward_msgs, marknoticed_chat, save_msgs, send_text_msg, ChatItem,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::reaction::send_reaction;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils as test;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_guess_msgtype_from_suffix() {
|
||||
assert_eq!(
|
||||
guess_msgtype_from_path_suffix(Path::new("foo/bar-sth.mp3")),
|
||||
Some((Viewtype::Audio, "audio/mpeg"))
|
||||
);
|
||||
assert_eq!(
|
||||
guess_msgtype_from_path_suffix(Path::new("foo/file.html")),
|
||||
Some((Viewtype::File, "text/html"))
|
||||
);
|
||||
assert_eq!(
|
||||
guess_msgtype_from_path_suffix(Path::new("foo/file.xdc")),
|
||||
Some((Viewtype::Webxdc, "application/webxdc+zip"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_webrtc_instance() {
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
|
||||
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
|
||||
assert_eq!(url, "https://foo/bar");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("bAsIcwEbrTc:url");
|
||||
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
|
||||
assert_eq!(url, "url");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("https://foo/bar?key=val#key=val");
|
||||
assert_eq!(webrtc_type, VideochatType::Unknown);
|
||||
assert_eq!(url, "https://foo/bar?key=val#key=val");
|
||||
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("jitsi:https://j.si/foo");
|
||||
assert_eq!(webrtc_type, VideochatType::Jitsi);
|
||||
assert_eq!(url, "https://j.si/foo");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_webrtc_instance() {
|
||||
// webrtc_instance may come from an input field of the ui, be pretty tolerant on input
|
||||
let instance = Message::create_webrtc_instance("https://meet.jit.si/", "123");
|
||||
assert_eq!(instance, "https://meet.jit.si/123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("https://meet.jit.si", "456");
|
||||
assert_eq!(instance, "https://meet.jit.si/456");
|
||||
|
||||
let instance = Message::create_webrtc_instance("meet.jit.si", "789");
|
||||
assert_eq!(instance, "https://meet.jit.si/789");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo?", "123");
|
||||
assert_eq!(instance, "https://bla.foo?123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("jitsi:bla.foo#", "456");
|
||||
assert_eq!(instance, "jitsi:https://bla.foo#456");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room=", "789");
|
||||
assert_eq!(instance, "https://bla.foo#room=789");
|
||||
|
||||
let instance = Message::create_webrtc_instance("https://bla.foo#room", "123");
|
||||
assert_eq!(instance, "https://bla.foo#room/123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room$ROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo#room123");
|
||||
|
||||
let instance = Message::create_webrtc_instance("bla.foo#room=$ROOM&after=cont", "234");
|
||||
assert_eq!(instance, "https://bla.foo#room=234&after=cont");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" meet.jit .si ", "789");
|
||||
assert_eq!(instance, "https://meet.jit.si/789");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" basicwebrtc: basic . stuff\n ", "12345ab");
|
||||
assert_eq!(instance, "basicwebrtc:https://basic.stuff/12345ab");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_webrtc_instance_noroom() {
|
||||
// webrtc_instance may come from an input field of the ui, be pretty tolerant on input
|
||||
let instance = Message::create_webrtc_instance("bla.foo$NOROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" bla . foo $NOROOM ", "456");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" $NOROOM bla . foo ", "789");
|
||||
assert_eq!(instance, "https://bla.foo");
|
||||
|
||||
let instance = Message::create_webrtc_instance(" bla.foo / $NOROOM ? a = b ", "123");
|
||||
assert_eq!(instance, "https://bla.foo/?a=b");
|
||||
|
||||
// $ROOM has a higher precedence
|
||||
let instance = Message::create_webrtc_instance("bla.foo/?$NOROOM=$ROOM", "123");
|
||||
assert_eq!(instance, "https://bla.foo/?$NOROOM=123");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_width_height() {
|
||||
let t = test::TestContext::new().await;
|
||||
|
||||
// test that get_width() and get_height() are returning some dimensions for images;
|
||||
// (as the device-chat contains a welcome-images, we check that)
|
||||
t.update_device_chats().await.ok();
|
||||
let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut has_image = false;
|
||||
let chatitems = chat::get_chat_msgs(&t, device_chat_id).await.unwrap();
|
||||
for chatitem in chatitems {
|
||||
if let ChatItem::Message { msg_id } = chatitem {
|
||||
if let Ok(msg) = Message::load_from_db(&t, msg_id).await {
|
||||
if msg.get_viewtype() == Viewtype::Image {
|
||||
has_image = true;
|
||||
// just check that width/height are inside some reasonable ranges
|
||||
assert!(msg.get_width() > 100);
|
||||
assert!(msg.get_height() > 100);
|
||||
assert!(msg.get_width() < 4000);
|
||||
assert!(msg.get_height() < 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(has_image);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quote() {
|
||||
let d = test::TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
ctx.set_config(Config::ConfiguredAddr, Some("self@example.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat = d.create_chat_with_contact("", "dest@example.com").await;
|
||||
|
||||
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());
|
||||
|
||||
let mut msg2 = Message::new(Viewtype::Text);
|
||||
msg2.set_quote(ctx, Some(&msg))
|
||||
.await
|
||||
.expect("can't set quote");
|
||||
assert_eq!(msg2.quoted_text().unwrap(), msg.get_text());
|
||||
|
||||
let quoted_msg = msg2
|
||||
.quoted_message(ctx)
|
||||
.await
|
||||
.expect("error while retrieving quoted message")
|
||||
.expect("quoted message not found");
|
||||
assert_eq!(quoted_msg.get_text(), msg2.quoted_text().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_quote() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.send_recv_accept(alice, bob, "Hi!").await;
|
||||
let msg = tcm
|
||||
.send_recv(
|
||||
alice,
|
||||
bob,
|
||||
"On 2024-08-28, Alice wrote:\n> A quote.\nNot really.",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(msg.quoted_text().is_none());
|
||||
assert!(msg.quoted_message(bob).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unencrypted_quote_encrypted_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_group = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "Group chat", &[bob])
|
||||
.await;
|
||||
let sent = alice.send_text(alice_group, "Hi! I created a group").await;
|
||||
let bob_received_message = bob.recv_msg(&sent).await;
|
||||
|
||||
let bob_group = bob_received_message.chat_id;
|
||||
bob_group.accept(bob).await?;
|
||||
let sent = bob.send_text(bob_group, "Encrypted message").await;
|
||||
let alice_received_message = alice.recv_msg(&sent).await;
|
||||
assert!(alice_received_message.get_showpadlock());
|
||||
|
||||
// Alice adds contact without key so chat becomes unencrypted.
|
||||
let alice_flubby_contact_id = Contact::create(alice, "Flubby", "flubby@example.org").await?;
|
||||
add_contact_to_chat(alice, alice_group, alice_flubby_contact_id).await?;
|
||||
|
||||
// Alice quotes encrypted message in unencrypted chat.
|
||||
let mut msg = Message::new_text("unencrypted".to_string());
|
||||
msg.set_quote(alice, Some(&alice_received_message)).await?;
|
||||
chat::send_msg(alice, alice_group, &mut msg).await?;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(bob_received_message.quoted_text().unwrap(), "...");
|
||||
assert_eq!(bob_received_message.get_showpadlock(), false);
|
||||
|
||||
// Alice replaces a quote of encrypted message with a quote of unencrypted one.
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.set_quote(alice, Some(&alice_received_message)).await?;
|
||||
msg1.set_quote(alice, Some(&msg)).await?;
|
||||
chat::send_msg(alice, alice_group, &mut msg1).await?;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(bob_received_message.quoted_text().unwrap(), "unencrypted");
|
||||
assert_eq!(bob_received_message.get_showpadlock(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_chat_id() {
|
||||
// Alice receives a message that pops up as a contact request
|
||||
let alice = TestContext::new_alice().await;
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <123@example.com>\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// check chat-id of this message
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert!(!msg.get_chat_id().is_special());
|
||||
assert_eq!(msg.get_text(), "hello".to_string());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_override_sender_name() {
|
||||
// send message with overridden sender name
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let contact_id = *chat::get_chat_contacts(&alice, chat.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await.unwrap();
|
||||
|
||||
let mut msg = Message::new_text("bla blubb".to_string());
|
||||
msg.set_override_sender_name(Some("over ride".to_string()));
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
Some("over ride".to_string())
|
||||
);
|
||||
assert_eq!(msg.get_sender_name(&contact), "over ride".to_string());
|
||||
assert_ne!(contact.get_display_name(), "over ride".to_string());
|
||||
chat::send_msg(&alice, chat.id, &mut msg).await.unwrap();
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// bob receives that message
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let contact_id = *chat::get_chat_contacts(&bob, chat.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&bob, contact_id).await.unwrap();
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
assert_eq!(msg.text, "bla blubb");
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
Some("over ride".to_string())
|
||||
);
|
||||
assert_eq!(msg.get_sender_name(&contact), "over ride".to_string());
|
||||
assert_ne!(contact.get_display_name(), "over ride".to_string());
|
||||
|
||||
// explicitly check that the message does not create a mailing list
|
||||
// (mailing lists may also use `Sender:`-header)
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await.unwrap();
|
||||
assert_ne!(chat.typ, Chattype::Mailinglist);
|
||||
|
||||
// Alice receives message on another device.
|
||||
let msg = alice2.recv_msg(&sent_msg).await;
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
Some("over ride".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_original_msg_id() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// normal sending of messages does not have an original ID
|
||||
let one2one_chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_text(one2one_chat.id, "foo").await;
|
||||
let orig_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
|
||||
assert!(orig_msg.get_original_msg_id(&alice).await?.is_none());
|
||||
assert!(orig_msg.parent(&alice).await?.is_none());
|
||||
assert!(orig_msg.quoted_message(&alice).await?.is_none());
|
||||
|
||||
// forwarding to "Saved Messages", the message gets the original ID attached
|
||||
let self_chat = alice.get_self_chat().await;
|
||||
save_msgs(&alice, &[sent.sender_msg_id]).await?;
|
||||
let saved_msg = alice.get_last_msg_in(self_chat.get_id()).await;
|
||||
assert_ne!(saved_msg.get_id(), orig_msg.get_id());
|
||||
assert_eq!(
|
||||
saved_msg.get_original_msg_id(&alice).await?.unwrap(),
|
||||
orig_msg.get_id()
|
||||
);
|
||||
assert!(saved_msg.parent(&alice).await?.is_none());
|
||||
assert!(saved_msg.quoted_message(&alice).await?.is_none());
|
||||
|
||||
// forwarding from "Saved Messages" back to another chat, detaches original ID
|
||||
forward_msgs(&alice, &[saved_msg.get_id()], one2one_chat.get_id()).await?;
|
||||
let forwarded_msg = alice.get_last_msg_in(one2one_chat.get_id()).await;
|
||||
assert_ne!(forwarded_msg.get_id(), saved_msg.get_id());
|
||||
assert_ne!(forwarded_msg.get_id(), orig_msg.get_id());
|
||||
assert!(forwarded_msg.get_original_msg_id(&alice).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_markseen_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
|
||||
// alice sends to bob,
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
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 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);
|
||||
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
// that has no effect in contact request
|
||||
markseen_msgs(&bob, vec![msg1.id, msg2.id]).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
let bob_chat = Chat::load_from_db(&bob, bob_chat_id).await?;
|
||||
assert_eq!(bob_chat.blocked, Blocked::Request);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
bob_chat_id.accept(&bob).await.unwrap();
|
||||
|
||||
// bob sends to alice,
|
||||
// alice knows bob and messages appear in normal chat
|
||||
let msg1 = alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
let msg2 = alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0)?, alice_chat.id);
|
||||
assert_eq!(chats.get_chat_id(0)?, msg1.chat_id);
|
||||
assert_eq!(chats.get_chat_id(0)?, msg2.chat_id);
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 2);
|
||||
|
||||
// no message-ids, that should have no effect
|
||||
markseen_msgs(&alice, vec![]).await?;
|
||||
|
||||
// bad message-id, that should have no effect
|
||||
markseen_msgs(&alice, vec![MsgId::new(123456)]).await?;
|
||||
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 2);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 2);
|
||||
|
||||
// mark the most recent as seen
|
||||
markseen_msgs(&alice, vec![msg2.id]).await?;
|
||||
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 1);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 1);
|
||||
|
||||
// user scrolled up - mark both as seen
|
||||
markseen_msgs(&alice, vec![msg1.id, msg2.id]).await?;
|
||||
|
||||
assert_eq!(alice_chat.id.get_fresh_msg_cnt(&alice).await?, 0);
|
||||
assert_eq!(alice.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_markseen_not_downloaded_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?;
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert!(!msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
// A not downloaded message can be seen only if it's seen on another device.
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
// Marking the message as seen again is a no op.
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::InProgress)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::Failure)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
msg.id
|
||||
.update_download_state(alice, DownloadState::Undecipherable)
|
||||
.await?;
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
assert_eq!(msg.id.get_state(alice).await?, MessageState::InNoticed);
|
||||
|
||||
assert!(
|
||||
!alice
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?
|
||||
);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
// Let's assume that Alice and Bob resolved the problem with encryption.
|
||||
let old_msg = msg;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, old_msg.chat_id);
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert!(msg.get_showpadlock());
|
||||
// The message state mustn't be downgraded to `InFresh`.
|
||||
assert_eq!(msg.state, MessageState::InNoticed);
|
||||
markseen_msgs(alice, vec![msg.id]).await?;
|
||||
let msg = Message::load_from_db(alice, msg.id).await?;
|
||||
assert_eq!(msg.state, MessageState::InSeen);
|
||||
assert_eq!(
|
||||
alice
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM smtp_mdns", ())
|
||||
.await?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_msg_seen_on_imap_when_downloaded() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(bob, "a.jpg", file_bytes, None)?;
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
let seen = true;
|
||||
let rcvd_msg = receive_imf(alice, sent_msg.payload().as_bytes(), seen)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(rcvd_msg.chat_id, msg.chat_id);
|
||||
let msg = Message::load_from_db(alice, *rcvd_msg.msg_ids.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert!(msg.param.get_bool(Param::WantsMdn).unwrap_or_default());
|
||||
assert!(msg.get_showpadlock());
|
||||
assert_eq!(msg.state, MessageState::InSeen);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_state() -> Result<()> {
|
||||
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;
|
||||
|
||||
// check both get_state() functions,
|
||||
// the one requiring a id and the one requiring an object
|
||||
async fn assert_state(t: &Context, msg_id: MsgId, state: MessageState) {
|
||||
assert_eq!(msg_id.get_state(t).await.unwrap(), state);
|
||||
assert_eq!(
|
||||
Message::load_from_db(t, msg_id).await.unwrap().get_state(),
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
// check outgoing messages states on sender side
|
||||
let mut alice_msg = Message::new_text("hi!".to_string());
|
||||
assert_eq!(alice_msg.get_state(), MessageState::Undefined); // message not yet in db, assert_state() won't work
|
||||
|
||||
alice_chat
|
||||
.id
|
||||
.set_draft(&alice, Some(&mut alice_msg))
|
||||
.await?;
|
||||
let mut alice_msg = alice_chat.id.get_draft(&alice).await?.unwrap();
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutDraft).await;
|
||||
|
||||
let msg_id = chat::send_msg(&alice, alice_chat.id, &mut alice_msg).await?;
|
||||
assert_eq!(msg_id, alice_msg.id);
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutPending).await;
|
||||
|
||||
let payload = alice.pop_sent_msg().await;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutDelivered).await;
|
||||
|
||||
set_msg_failed(&alice, &mut alice_msg, "badly failed").await?;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutFailed).await;
|
||||
|
||||
// check incoming message states on receiver side
|
||||
let bob_msg = bob.recv_msg(&payload).await;
|
||||
assert_eq!(bob_chat.id, bob_msg.chat_id);
|
||||
assert_state(&bob, bob_msg.id, MessageState::InFresh).await;
|
||||
|
||||
marknoticed_chat(&bob, bob_msg.chat_id).await?;
|
||||
assert_state(&bob, bob_msg.id, MessageState::InNoticed).await;
|
||||
|
||||
markseen_msgs(&bob, vec![bob_msg.id]).await?;
|
||||
assert_state(&bob, bob_msg.id, MessageState::InSeen).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_is_bot() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Alice receives an auto-generated non-chat message.
|
||||
//
|
||||
// This could be a holiday notice,
|
||||
// in which case the message should be marked as bot-generated,
|
||||
// but the contact should not.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Claire <claire@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <789@example.com>\n\
|
||||
Auto-Submitted: auto-generated\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), "hello".to_string());
|
||||
assert!(msg.is_bot());
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert!(!contact.is_bot());
|
||||
|
||||
// Alice receives a message from Bob the bot.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <123@example.com>\n\
|
||||
Auto-Submitted: auto-generated\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), "hello".to_string());
|
||||
assert!(msg.is_bot());
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert!(contact.is_bot());
|
||||
|
||||
// Alice receives a message from Bob who is not the bot anymore.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"From: Bob <bob@example.com>\n\
|
||||
To: alice@example.org\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <456@example.com>\n\
|
||||
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
|
||||
\n\
|
||||
hello again\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), "hello again".to_string());
|
||||
assert!(!msg.is_bot());
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert!(!contact.is_bot());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_viewtype_derive_display_works_as_expected() {
|
||||
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_viewtype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::default());
|
||||
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
|
||||
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
|
||||
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
|
||||
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
|
||||
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
|
||||
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
|
||||
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
|
||||
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
|
||||
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
|
||||
assert_eq!(
|
||||
Viewtype::VideochatInvitation,
|
||||
Viewtype::from_i32(70).unwrap()
|
||||
);
|
||||
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
|
||||
assert_eq!(Viewtype::Vcard, Viewtype::from_i32(90).unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_quotes() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
|
||||
let sent = alice.send_text(chat.id, "> First quote").await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, "> First quote");
|
||||
assert!(received.quoted_text().is_none());
|
||||
assert!(received.quoted_message(&bob).await?.is_none());
|
||||
|
||||
let sent = alice.send_text(chat.id, "> Second quote").await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, "> Second quote");
|
||||
assert!(received.quoted_text().is_none());
|
||||
assert!(received.quoted_message(&bob).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_message_summary_text() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.get_self_chat().await;
|
||||
let msg_id = send_text_msg(&t, chat.id, "foo".to_string()).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
let summary = msg.get_summary(&t, None).await?;
|
||||
assert_eq!(summary.text, "foo");
|
||||
|
||||
// message summary does not change when reactions are applied (in contrast to chatlist summary)
|
||||
send_reaction(&t, msg_id, "🫵").await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
let summary = msg.get_summary(&t, None).await?;
|
||||
assert_eq!(summary.text, "foo");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_format_flowed_round_trip() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
|
||||
let text = " Foo bar";
|
||||
let sent = alice.send_text(chat.id, text).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, text);
|
||||
|
||||
let text = "Foo bar baz";
|
||||
let sent = alice.send_text(chat.id, text).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, text);
|
||||
|
||||
let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
|
||||
let sent = alice.send_text(chat.id, text).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, text);
|
||||
|
||||
let python_program = "\
|
||||
def hello():
|
||||
return 'Hello, world!'";
|
||||
let sent = alice.send_text(chat.id, python_program).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.text, python_program);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_msgs_offline() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
delete_msgs(&alice, &[msg.id]).await?;
|
||||
assert!(!alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
//! # MIME message production.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
@@ -155,7 +154,6 @@ fn new_address_with_name(name: &str, address: String) -> Address {
|
||||
|
||||
impl MimeFactory {
|
||||
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
|
||||
let now = time();
|
||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||
let attach_profile_data = Self::should_attach_profile_data(&msg);
|
||||
let undisclosed_recipients = chat.typ == Chattype::Broadcast;
|
||||
@@ -186,8 +184,8 @@ impl MimeFactory {
|
||||
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()));
|
||||
}
|
||||
to.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
} else if chat.is_mailing_list() {
|
||||
let list_post = chat
|
||||
.param
|
||||
@@ -208,7 +206,7 @@ impl MimeFactory {
|
||||
"SELECT c.authname, c.addr, c.id, cc.add_timestamp, cc.remove_timestamp
|
||||
FROM chats_contacts cc
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id
|
||||
WHERE cc.chat_id=? AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))",
|
||||
WHERE cc.chat_id=? AND cc.contact_id>9 OR (cc.contact_id=1 AND ?)",
|
||||
(msg.chat_id, chat.typ == Chattype::Group),
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
@@ -241,7 +239,7 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
recipient_ids.insert(id);
|
||||
} else if remove_timestamp.saturating_add(60 * 24 * 3600) > now {
|
||||
} else {
|
||||
// Row is a tombstone,
|
||||
// member is not actually part of the group.
|
||||
if !recipients_contain_addr(&past_members, &addr) {
|
||||
@@ -622,23 +620,18 @@ impl MimeFactory {
|
||||
);
|
||||
}
|
||||
|
||||
if let Loaded::Message { chat, .. } = &self.loaded {
|
||||
if chat.typ == Chattype::Group
|
||||
&& !self.member_timestamps.is_empty()
|
||||
&& !chat.member_list_is_stale(context).await?
|
||||
{
|
||||
headers.push(
|
||||
Header::new_with_value(
|
||||
"Chat-Group-Member-Timestamps".into(),
|
||||
self.member_timestamps
|
||||
.iter()
|
||||
.map(|ts| ts.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
if !self.member_timestamps.is_empty() {
|
||||
headers.push(
|
||||
Header::new_with_value(
|
||||
"Chat-Group-Member-Timestamps".into(),
|
||||
self.member_timestamps
|
||||
.iter()
|
||||
.map(|ts| ts.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let subject_str = self.subject_str(context).await?;
|
||||
@@ -1149,6 +1142,7 @@ impl MimeFactory {
|
||||
part.body(text)
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
async fn render_message(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -1611,17 +1605,12 @@ pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
|
||||
}
|
||||
|
||||
async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder> {
|
||||
let file_name = msg.get_filename().context("msg has no file")?;
|
||||
let suffix = Path::new(&file_name)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("dat");
|
||||
|
||||
let blob = msg
|
||||
.param
|
||||
.get_blob(Param::File, context)
|
||||
.await?
|
||||
.context("msg has no file")?;
|
||||
let suffix = blob.suffix().unwrap_or("dat");
|
||||
|
||||
// Get file name to use for sending. For privacy purposes, we do
|
||||
// not transfer the original filenames eg. for images; these names
|
||||
@@ -1661,14 +1650,18 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result<PartBuilder
|
||||
),
|
||||
&suffix
|
||||
),
|
||||
_ => file_name,
|
||||
_ => msg
|
||||
.param
|
||||
.get(Param::Filename)
|
||||
.unwrap_or_else(|| blob.as_file_name())
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
/* check mimetype */
|
||||
let mimetype: mime::Mime = match msg.param.get(Param::MimeType) {
|
||||
Some(mtype) => mtype.parse()?,
|
||||
None => {
|
||||
if let Some(res) = message::guess_msgtype_from_suffix(msg) {
|
||||
if let Some(res) = message::guess_msgtype_from_suffix(blob.as_rel_path()) {
|
||||
res.1.parse()?
|
||||
} else {
|
||||
mime::APPLICATION_OCTET_STREAM
|
||||
@@ -2631,7 +2624,8 @@ mod tests {
|
||||
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
|
||||
// decoded_data to check presence of the necessary headers.
|
||||
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)?;
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)
|
||||
.await?;
|
||||
let sent = bob.send_msg(chat, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
assert!(sent.payload.contains("\r\nSubject: [...]\r\n"));
|
||||
@@ -2648,40 +2642,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_remove_self() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let first_group = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob])
|
||||
.await;
|
||||
alice.send_text(first_group, "Hi! I created a group.").await;
|
||||
remove_contact_from_chat(alice, first_group, ContactId::SELF).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
let second_group = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "First group", &[bob])
|
||||
.await;
|
||||
let sent = alice
|
||||
.send_text(second_group, "Hi! I created another group.")
|
||||
.await;
|
||||
|
||||
println!("{}", sent.payload);
|
||||
let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
mime_message.get_header(HeaderDef::ChatGroupPastMembers),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
mime_message.chat_group_member_timestamps().unwrap().len(),
|
||||
1 // There is a timestamp for Bob, not for Alice
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
1882
src/mimeparser.rs
1882
src/mimeparser.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ use crate::context::Context;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::wrap_rustls;
|
||||
use crate::tools::time;
|
||||
use crate::tools::{create_id, time};
|
||||
|
||||
/// HTTP(S) GET response.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -119,8 +119,12 @@ fn http_url_cache_timestamps(url: &str, mimetype: Option<&str>) -> (i64, i64) {
|
||||
|
||||
/// Places the binary into HTTP cache.
|
||||
async fn http_cache_put(context: &Context, url: &str, response: &Response) -> Result<()> {
|
||||
let blob =
|
||||
BlobObject::create_and_deduplicate_from_bytes(context, response.blob.as_slice(), "")?;
|
||||
let blob = BlobObject::create(
|
||||
context,
|
||||
&format!("http_cache_{}", create_id()),
|
||||
response.blob.as_slice(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (expires, stale) = http_url_cache_timestamps(url, response.mimetype.as_deref());
|
||||
context
|
||||
|
||||
@@ -183,6 +183,8 @@ pub enum Param {
|
||||
GroupNameTimestamp = b'g',
|
||||
|
||||
/// For Chats: timestamp of member list update.
|
||||
///
|
||||
/// Deprecated 2025-01-07.
|
||||
MemberListTimestamp = b'k',
|
||||
|
||||
/// For Webxdc Message Instances: Current document name
|
||||
@@ -373,6 +375,7 @@ impl Params {
|
||||
/// Note that in the [ParamsFile::FsPath] case the blob can be
|
||||
/// created without copying if the path already refers to a valid
|
||||
/// blob. If so a [BlobObject] will be returned.
|
||||
#[allow(clippy::needless_lifetimes)]
|
||||
pub async fn get_blob<'a>(
|
||||
&self,
|
||||
key: Param,
|
||||
|
||||
@@ -24,12 +24,14 @@
|
||||
//! and `joinRealtimeChannel().send()` just like the other peers.
|
||||
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use email::Header;
|
||||
use futures_lite::StreamExt;
|
||||
use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMap, RelayMode, RelayUrl, SecretKey};
|
||||
use iroh_gossip::net::{Event, Gossip, GossipEvent, JoinOptions, GOSSIP_ALPN};
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use iroh_net::key::{PublicKey, SecretKey};
|
||||
use iroh_net::relay::{RelayMap, RelayUrl};
|
||||
use iroh_net::{relay::RelayMode, Endpoint};
|
||||
use iroh_net::{NodeAddr, NodeId};
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::env;
|
||||
@@ -52,8 +54,8 @@ const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
|
||||
/// Store iroh peer channels for the context.
|
||||
#[derive(Debug)]
|
||||
pub struct Iroh {
|
||||
/// iroh router needed for iroh peer channels.
|
||||
pub(crate) router: iroh::protocol::Router,
|
||||
/// [Endpoint] needed for iroh peer channels.
|
||||
pub(crate) endpoint: Endpoint,
|
||||
|
||||
/// [Gossip] needed for iroh peer channels.
|
||||
pub(crate) gossip: Gossip,
|
||||
@@ -73,12 +75,15 @@ pub struct Iroh {
|
||||
impl Iroh {
|
||||
/// Notify the endpoint that the network has changed.
|
||||
pub(crate) async fn network_change(&self) {
|
||||
self.router.endpoint().network_change().await
|
||||
self.endpoint.network_change().await
|
||||
}
|
||||
|
||||
/// Closes the QUIC endpoint.
|
||||
pub(crate) async fn close(self) -> Result<()> {
|
||||
self.router.shutdown().await.context("Closing iroh failed")
|
||||
self.endpoint
|
||||
.close(0u32.into(), b"")
|
||||
.await
|
||||
.context("Closing iroh endpoint failed")
|
||||
}
|
||||
|
||||
/// Join a topic and create the subscriber loop for it.
|
||||
@@ -115,8 +120,8 @@ impl Iroh {
|
||||
|
||||
// Inform iroh of potentially new node addresses
|
||||
for node_addr in &peers {
|
||||
if !node_addr.is_empty() {
|
||||
self.router.endpoint().add_node_addr(node_addr.clone())?;
|
||||
if !node_addr.info.is_empty() {
|
||||
self.endpoint.add_node_addr(node_addr.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +129,7 @@ impl Iroh {
|
||||
|
||||
let (gossip_sender, gossip_receiver) = self
|
||||
.gossip
|
||||
.subscribe_with_opts(topic, JoinOptions::with_bootstrap(node_ids))
|
||||
.join_with_opts(topic, JoinOptions::with_bootstrap(node_ids))
|
||||
.split();
|
||||
|
||||
let ctx = ctx.clone();
|
||||
@@ -143,10 +148,10 @@ impl Iroh {
|
||||
pub async fn maybe_add_gossip_peers(&self, topic: TopicId, peers: Vec<NodeAddr>) -> Result<()> {
|
||||
if self.iroh_channels.read().await.get(&topic).is_some() {
|
||||
for peer in &peers {
|
||||
self.router.endpoint().add_node_addr(peer.clone())?;
|
||||
self.endpoint.add_node_addr(peer.clone())?;
|
||||
}
|
||||
|
||||
self.gossip.subscribe_with_opts(
|
||||
self.gossip.join_with_opts(
|
||||
topic,
|
||||
JoinOptions::with_bootstrap(peers.into_iter().map(|peer| peer.node_id)),
|
||||
);
|
||||
@@ -193,8 +198,8 @@ impl Iroh {
|
||||
|
||||
/// Get the iroh [NodeAddr] without direct IP addresses.
|
||||
pub(crate) async fn get_node_addr(&self) -> Result<NodeAddr> {
|
||||
let mut addr = self.router.endpoint().node_addr().await?;
|
||||
addr.direct_addresses = BTreeSet::new();
|
||||
let mut addr = self.endpoint.node_addr().await?;
|
||||
addr.info.direct_addresses = BTreeSet::new();
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
@@ -237,7 +242,7 @@ impl Context {
|
||||
/// Create iroh endpoint and gossip.
|
||||
async fn init_peer_channels(&self) -> Result<Iroh> {
|
||||
info!(self, "Initializing peer channels.");
|
||||
let secret_key = SecretKey::generate(rand::rngs::OsRng);
|
||||
let secret_key = SecretKey::generate();
|
||||
let public_key = secret_key.public();
|
||||
|
||||
let relay_mode = if let Some(relay_url) = self
|
||||
@@ -262,22 +267,24 @@ impl Context {
|
||||
.await?;
|
||||
|
||||
// create gossip
|
||||
// Allow messages up to 128 KB in size.
|
||||
// We set the limit to 128 KiB to account for internal overhead,
|
||||
// but only guarantee 128 KB of payload to WebXDC developers.
|
||||
let my_addr = endpoint.node_addr().await?;
|
||||
let gossip_config = iroh_gossip::proto::topic::Config {
|
||||
// Allow messages up to 128 KB in size.
|
||||
// We set the limit to 128 KiB to account for internal overhead,
|
||||
// but only guarantee 128 KB of payload to WebXDC developers.
|
||||
max_message_size: 128 * 1024,
|
||||
..Default::default()
|
||||
};
|
||||
let gossip = Gossip::from_endpoint(endpoint.clone(), gossip_config, &my_addr.info);
|
||||
|
||||
let gossip = Gossip::builder()
|
||||
.max_message_size(128 * 1024)
|
||||
.spawn(endpoint.clone())
|
||||
.await?;
|
||||
// spawn endpoint loop that forwards incoming connections to the gossiper
|
||||
let context = self.clone();
|
||||
|
||||
let router = iroh::protocol::Router::builder(endpoint)
|
||||
.accept(GOSSIP_ALPN, gossip.clone())
|
||||
.spawn()
|
||||
.await?;
|
||||
// Shuts down on deltachat shutdown
|
||||
tokio::spawn(endpoint_loop(context, endpoint.clone(), gossip.clone()));
|
||||
|
||||
Ok(Iroh {
|
||||
router,
|
||||
endpoint,
|
||||
gossip,
|
||||
sequence_numbers: Mutex::new(HashMap::new()),
|
||||
iroh_channels: RwLock::new(HashMap::new()),
|
||||
@@ -499,13 +506,54 @@ fn create_random_topic() -> TopicId {
|
||||
pub(crate) async fn create_iroh_header(ctx: &Context, msg_id: MsgId) -> Result<Header> {
|
||||
let topic = create_random_topic();
|
||||
insert_topic_stub(ctx, msg_id, topic).await?;
|
||||
let topic_string = BASE32_NOPAD.encode(topic.as_bytes()).to_ascii_lowercase();
|
||||
Ok(Header::new(
|
||||
HeaderDef::IrohGossipTopic.get_headername().to_string(),
|
||||
topic_string,
|
||||
topic.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn endpoint_loop(context: Context, endpoint: Endpoint, gossip: Gossip) {
|
||||
while let Some(conn) = endpoint.accept().await {
|
||||
let conn = match conn.accept() {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to accept iroh connection: {err:#}.");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
info!(context, "IROH_REALTIME: accepting iroh connection");
|
||||
let gossip = gossip.clone();
|
||||
let context = context.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = handle_connection(&context, conn, gossip).await {
|
||||
warn!(context, "IROH_REALTIME: iroh connection error: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
context: &Context,
|
||||
mut conn: iroh_net::endpoint::Connecting,
|
||||
gossip: Gossip,
|
||||
) -> anyhow::Result<()> {
|
||||
let alpn = conn.alpn().await?;
|
||||
let conn = conn.await?;
|
||||
let peer_id = iroh_net::endpoint::get_remote_node_id(&conn)?;
|
||||
|
||||
match alpn.as_slice() {
|
||||
GOSSIP_ALPN => gossip
|
||||
.handle_connection(conn)
|
||||
.await
|
||||
.context(format!("Gossip connection to {peer_id} failed"))?,
|
||||
_ => warn!(
|
||||
context,
|
||||
"Ignoring connection from {peer_id}: unsupported ALPN protocol"
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn subscribe_loop(
|
||||
context: &Context,
|
||||
mut stream: iroh_gossip::net::GossipReceiver,
|
||||
@@ -580,6 +628,7 @@ mod tests {
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
|
||||
@@ -751,6 +800,7 @@ mod tests {
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
|
||||
@@ -934,6 +984,7 @@ mod tests {
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
|
||||
let alice_webxdc = alice.get_last_msg().await;
|
||||
|
||||
@@ -21,9 +21,11 @@ use tokio::runtime::Handle;
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[cfg(test)]
|
||||
pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub const HEADER_SETUPCODE: &str = "passphrase-begin";
|
||||
|
||||
/// Preferred symmetric encryption algorithm.
|
||||
@@ -379,6 +381,13 @@ pub fn pk_validate(
|
||||
|
||||
let standalone_signature = StandaloneSignature::from_armor_single(Cursor::new(signature))?.0;
|
||||
|
||||
// Remove trailing CRLF before the delimiter.
|
||||
// According to RFC 3156 it is considered to be part of the MIME delimiter for the purpose of
|
||||
// OpenPGP signature calculation.
|
||||
let content = content
|
||||
.get(..content.len().saturating_sub(2))
|
||||
.context("index is out of range")?;
|
||||
|
||||
for pkey in public_keys_for_validation {
|
||||
if standalone_signature.verify(pkey, content).is_ok() {
|
||||
let fp = pkey.dc_fingerprint();
|
||||
|
||||
@@ -4,7 +4,7 @@ pub(crate) mod data;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use hickory_resolver::{config, Resolver, TokioResolver};
|
||||
use hickory_resolver::{config, AsyncResolver, TokioAsyncResolver};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
@@ -165,11 +165,11 @@ impl ProviderOptions {
|
||||
/// We first try to read the system's resolver from `/etc/resolv.conf`.
|
||||
/// This does not work at least on some Androids, therefore we fallback
|
||||
/// to the default `ResolverConfig` which uses eg. to google's `8.8.8.8` or `8.8.4.4`.
|
||||
fn get_resolver() -> Result<TokioResolver> {
|
||||
if let Ok(resolver) = Resolver::tokio_from_system_conf() {
|
||||
fn get_resolver() -> Result<TokioAsyncResolver> {
|
||||
if let Ok(resolver) = AsyncResolver::tokio_from_system_conf() {
|
||||
return Ok(resolver);
|
||||
}
|
||||
let resolver = Resolver::tokio(
|
||||
let resolver = AsyncResolver::tokio(
|
||||
config::ResolverConfig::default(),
|
||||
config::ResolverOpts::default(),
|
||||
);
|
||||
|
||||
923
src/qr.rs
923
src/qr.rs
@@ -112,7 +112,7 @@ pub enum Qr {
|
||||
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
|
||||
Backup2 {
|
||||
/// Iroh node address.
|
||||
node_addr: iroh::NodeAddr,
|
||||
node_addr: iroh_net::NodeAddr,
|
||||
|
||||
/// Authentication token.
|
||||
auth_token: String,
|
||||
@@ -261,13 +261,6 @@ pub enum Qr {
|
||||
},
|
||||
}
|
||||
|
||||
// hack around the changed JSON accidentally used by an iroh upgrade, see #6518 for more details and for code snippet.
|
||||
// this hack is mainly needed to give ppl time to upgrade and can be removed after some months (added 2025-02)
|
||||
fn fix_add_second_device_qr(qr: &str) -> String {
|
||||
qr.replacen(r#","info":{"relay_url":"#, r#","relay_url":"#, 1)
|
||||
.replacen(r#""]}}"#, r#""]}"#, 1)
|
||||
}
|
||||
|
||||
fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
|
||||
string.to_lowercase().starts_with(&pattern.to_lowercase())
|
||||
}
|
||||
@@ -297,8 +290,7 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
} else if qr.starts_with(SHADOWSOCKS_SCHEME) {
|
||||
decode_shadowsocks_proxy(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCBACKUP2_SCHEME) {
|
||||
let qr_fixed = fix_add_second_device_qr(qr);
|
||||
decode_backup2(&qr_fixed)?
|
||||
decode_backup2(qr)?
|
||||
} else if qr.starts_with(MAILTO_SCHEME) {
|
||||
decode_mailto(context, qr).await?
|
||||
} else if qr.starts_with(SMTP_SCHEME) {
|
||||
@@ -652,7 +644,7 @@ fn decode_backup2(qr: &str) -> Result<Qr> {
|
||||
.split_once('&')
|
||||
.context("Backup QR code has no separator")?;
|
||||
let auth_token = auth_token.to_string();
|
||||
let node_addr = serde_json::from_str::<iroh::NodeAddr>(node_addr)
|
||||
let node_addr = serde_json::from_str::<iroh_net::NodeAddr>(node_addr)
|
||||
.context("Invalid node addr in backup QR code")?;
|
||||
|
||||
Ok(Qr::Backup2 {
|
||||
@@ -955,4 +947,911 @@ fn normalize_address(addr: &str) -> Result<String> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod qr_tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::key::DcKey;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_http() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com:80").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "http://www.hello.com:80".to_string(),
|
||||
host: "www.hello.com".to_string(),
|
||||
port: 80
|
||||
}
|
||||
);
|
||||
|
||||
// If it has no explicit port, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// If it has a path, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com/").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com/".to_string(),
|
||||
}
|
||||
);
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com/hello").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com/hello".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// Test that QR code whitespace is stripped.
|
||||
// Users can copy-paste QR code contents and "scan"
|
||||
// from the clipboard.
|
||||
let qr = check_qr(&ctx.ctx, " \thttp://www.hello.com/hello \n\t \r\n ").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com/hello".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_https() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com:443").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "https://www.hello.com:443".to_string(),
|
||||
host: "www.hello.com".to_string(),
|
||||
port: 443
|
||||
}
|
||||
);
|
||||
|
||||
// If it has no explicit port, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://www.hello.com".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// If it has a path, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com/").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://www.hello.com/".to_string(),
|
||||
}
|
||||
);
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com/hello").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://www.hello.com/hello".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_text() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "I am so cool").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Text {
|
||||
text: "I am so cool".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_vcard() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD"
|
||||
).await?;
|
||||
|
||||
if let Qr::Addr { contact_id, draft } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||
assert_eq!(contact.get_name(), "First Last");
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_display_name(), "First Last");
|
||||
assert!(draft.is_none());
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_matmsg() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"MATMSG:TO:\n\nstress@test.local ; \n\nSUB:\n\nSubject here\n\nBODY:\n\nhelloworld\n;;",
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Qr::Addr { contact_id, draft } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||
assert!(draft.is_none());
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_mailto() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"mailto:stress@test.local?subject=hello&body=beautiful+world",
|
||||
)
|
||||
.await?;
|
||||
if let Qr::Addr { contact_id, draft } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||
assert_eq!(draft.unwrap(), "hello\nbeautiful world");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org").await?;
|
||||
if let Qr::Addr { contact_id, draft } = res {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "no-questionmark@example.org");
|
||||
assert!(draft.is_none());
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
let res = check_qr(&ctx.ctx, "mailto:no-addr").await;
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_smtp() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
if let Qr::Addr { contact_id, draft } =
|
||||
check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await?
|
||||
{
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||
assert!(draft.is_none());
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_ideltachat_link() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"https://i.delta.chat/#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
).await?;
|
||||
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"https://i.delta.chat#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
).await?;
|
||||
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too.
|
||||
// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7%23a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
).await?;
|
||||
|
||||
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_group() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
).await?;
|
||||
if let Qr::AskVerifyGroup {
|
||||
contact_id,
|
||||
grpname,
|
||||
..
|
||||
} = qr
|
||||
{
|
||||
assert_ne!(contact_id, ContactId::UNDEFINED);
|
||||
assert_eq!(grpname, "test ? test !");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
let ctx = TestContext::new().await;
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
).await?;
|
||||
if let Qr::AskVerifyGroup {
|
||||
contact_id,
|
||||
grpname,
|
||||
..
|
||||
} = qr
|
||||
{
|
||||
assert_ne!(contact_id, ContactId::UNDEFINED);
|
||||
assert_eq!(grpname, "test ? test !");
|
||||
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_invalid_token() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
// Token cannot contain "/"
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL/cxRL"
|
||||
).await?;
|
||||
|
||||
assert!(matches!(qr, Qr::FprMismatch { .. }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_secure_join() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||
).await?;
|
||||
|
||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||
assert_ne!(contact_id, ContactId::UNDEFINED);
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||
).await?;
|
||||
|
||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||
assert_eq!(contact.get_authname(), "Jörn P. P.");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
// Regression test
|
||||
let ctx = TestContext::new().await;
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||
).await?;
|
||||
|
||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.org")
|
||||
.await
|
||||
.context("failed to create contact")?;
|
||||
let pub_key = alice_keypair().public;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.org".to_string(),
|
||||
last_seen: 1,
|
||||
last_seen_autocrypt: 1,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(pub_key.clone()),
|
||||
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
gossip_key: None,
|
||||
gossip_timestamp: 0,
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
backward_verified_key_id: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
assert!(
|
||||
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
|
||||
"failed to save peerstate"
|
||||
);
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprMismatch { contact_id, .. } = qr {
|
||||
assert_eq!(contact_id, Some(alice_contact_id));
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
&format!(
|
||||
"OPENPGP4FPR:{}#a=alice@example.org",
|
||||
pub_key.dc_fingerprint()
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprOk { contact_id, .. } = qr {
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org",
|
||||
)
|
||||
.await?,
|
||||
Qr::FprMismatch { contact_id: None }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_without_addr() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::FprWithoutAddr {
|
||||
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::FprWithoutAddr {
|
||||
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await;
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_withdraw_verifycontact() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let qr = get_securejoin_qr(&alice, None).await?;
|
||||
|
||||
// scanning own verify-contact code offers withdrawing
|
||||
assert!(matches!(
|
||||
check_qr(&alice, &qr).await?,
|
||||
Qr::WithdrawVerifyContact { .. }
|
||||
));
|
||||
set_config_from_qr(&alice, &qr).await?;
|
||||
|
||||
// scanning withdrawn verify-contact code offers reviving
|
||||
assert!(matches!(
|
||||
check_qr(&alice, &qr).await?,
|
||||
Qr::ReviveVerifyContact { .. }
|
||||
));
|
||||
set_config_from_qr(&alice, &qr).await?;
|
||||
assert!(matches!(
|
||||
check_qr(&alice, &qr).await?,
|
||||
Qr::WithdrawVerifyContact { .. }
|
||||
));
|
||||
|
||||
// someone else always scans as ask-verify-contact
|
||||
let bob = TestContext::new_bob().await;
|
||||
assert!(matches!(
|
||||
check_qr(&bob, &qr).await?,
|
||||
Qr::AskVerifyContact { .. }
|
||||
));
|
||||
assert!(set_config_from_qr(&bob, &qr).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_withdraw_verifygroup() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let qr = get_securejoin_qr(&alice, Some(chat_id)).await?;
|
||||
|
||||
// scanning own verify-group code offers withdrawing
|
||||
if let Qr::WithdrawVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? {
|
||||
assert_eq!(grpname, "foo");
|
||||
} else {
|
||||
bail!("Wrong QR type, expected WithdrawVerifyGroup");
|
||||
}
|
||||
set_config_from_qr(&alice, &qr).await?;
|
||||
|
||||
// scanning withdrawn verify-group code offers reviving
|
||||
if let Qr::ReviveVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? {
|
||||
assert_eq!(grpname, "foo");
|
||||
} else {
|
||||
bail!("Wrong QR type, expected ReviveVerifyGroup");
|
||||
}
|
||||
|
||||
// someone else always scans as ask-verify-group
|
||||
let bob = TestContext::new_bob().await;
|
||||
if let Qr::AskVerifyGroup { grpname, .. } = check_qr(&bob, &qr).await? {
|
||||
assert_eq!(grpname, "foo");
|
||||
} else {
|
||||
bail!("Wrong QR type, expected AskVerifyGroup");
|
||||
}
|
||||
assert!(set_config_from_qr(&bob, &qr).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_and_apply_dclogin() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let result = check_qr(&ctx.ctx, "dclogin:usename+extension@host?p=1234&v=1").await?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "usename+extension@host".to_owned());
|
||||
|
||||
if let LoginOptions::V1 { mail_pw, .. } = options {
|
||||
assert_eq!(mail_pw, "1234".to_owned());
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
|
||||
assert!(ctx.ctx.get_config(Config::Addr).await?.is_none());
|
||||
assert!(ctx.ctx.get_config(Config::MailPw).await?.is_none());
|
||||
|
||||
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&v=1").await?;
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::Addr).await?,
|
||||
Some("username+extension@host".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailPw).await?,
|
||||
Some("1234".to_owned())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_and_apply_dclogin_advanced_options() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&spw=4321&sh=send.host&sp=7273&su=SendUser&ih=host.tld&ip=4343&iu=user&ipw=password&is=ssl&ic=1&sc=3&ss=plain&v=1").await?;
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::Addr).await?,
|
||||
Some("username+extension@host".to_owned())
|
||||
);
|
||||
|
||||
// `p=1234` is ignored, because `ipw=password` is set
|
||||
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailServer).await?,
|
||||
Some("host.tld".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailPort).await?,
|
||||
Some("4343".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailUser).await?,
|
||||
Some("user".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailPw).await?,
|
||||
Some("password".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailSecurity).await?,
|
||||
Some("1".to_owned()) // ssl
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::ImapCertificateChecks).await?,
|
||||
Some("1".to_owned())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendPw).await?,
|
||||
Some("4321".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendServer).await?,
|
||||
Some("send.host".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendPort).await?,
|
||||
Some("7273".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendUser).await?,
|
||||
Some("SendUser".to_owned())
|
||||
);
|
||||
|
||||
// `sc` option is actually ignored and `ic` is used instead
|
||||
// because `smtp_certificate_checks` is deprecated.
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SmtpCertificateChecks).await?,
|
||||
Some("1".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendSecurity).await?,
|
||||
Some("3".to_owned()) // plain
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_account() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Account {
|
||||
domain: "example.org".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
// Test it again with lowercased "dcaccount:" uri scheme
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Account {
|
||||
domain: "example.org".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_webrtc_instance() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://basicurl.com/$ROOM").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::WebrtcInstance {
|
||||
domain: "basicurl.com".to_string(),
|
||||
instance_pattern: "basicwebrtc:https://basicurl.com/$ROOM".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
// Test it again with mixcased "dcWebRTC:" uri scheme
|
||||
let qr = check_qr(&ctx.ctx, "dcWebRTC:https://example.org/").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::WebrtcInstance {
|
||||
domain: "example.org".to_string(),
|
||||
instance_pattern: "https://example.org/".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_tg_socks_proxy() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://84.53.239.95:4145".to_string(),
|
||||
host: "84.53.239.95".to_string(),
|
||||
port: 4145,
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://foo.bar:123".to_string(),
|
||||
host: "foo.bar".to_string(),
|
||||
port: 123,
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://foo.baz:1080".to_string(),
|
||||
host: "foo.baz".to_string(),
|
||||
port: 1080,
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(
|
||||
&t,
|
||||
"https://t.me/socks?server=foo.baz&port=12345&user=ada&pass=ms%21%2F%24",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://ada:ms%21%2F%24@foo.baz:12345".to_string(),
|
||||
host: "foo.baz".to_string(),
|
||||
port: 12345,
|
||||
}
|
||||
);
|
||||
|
||||
// wrong domain results in Qr:Url instead of Qr::Socks5Proxy
|
||||
let qr = check_qr(&t, "https://not.me/socks?noserver=84.53.239.95&port=4145").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://not.me/socks?noserver=84.53.239.95&port=4145".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?noserver=84.53.239.95&port=4145").await;
|
||||
assert!(qr.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_account_bad_scheme() {
|
||||
let ctx = TestContext::new().await;
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"DCACCOUNT:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
|
||||
// Test it again with lowercased "dcaccount:" uri scheme
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"dcaccount:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_webrtc_instance_config_from_qr() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
|
||||
assert!(res.is_err());
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
|
||||
"https://example.org/"
|
||||
);
|
||||
|
||||
let res =
|
||||
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
|
||||
"basicwebrtc:https://foo.bar/?$ROOM&test"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_proxy_config_from_qr() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false);
|
||||
|
||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some("socks5://foo:666".to_string())
|
||||
);
|
||||
|
||||
// Test URL without port.
|
||||
//
|
||||
// Also check that whitespace is trimmed.
|
||||
let res = set_config_from_qr(&t, " https://t.me/socks?server=1.2.3.4\n").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some("socks5://1.2.3.4:1080\nsocks5://foo:666".to_string())
|
||||
);
|
||||
|
||||
// make sure, user&password are set when specified in the URL
|
||||
// Password is an URL-encoded "x&%$X".
|
||||
let res =
|
||||
set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"socks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080\nsocks5://foo:666"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
// Scanning existing proxy brings it to the top in the list.
|
||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"socks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
set_config_from_qr(
|
||||
&t,
|
||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
// SOCKS5 config does not have port 1080 explicitly specified,
|
||||
// but should bring `socks5://1.2.3.4:1080` to the top instead of creating another entry.
|
||||
set_config_from_qr(&t, "socks5://1.2.3.4").await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"socks5://1.2.3.4:1080\nss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_shadowsocks() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1".to_string(),
|
||||
host: "192.168.100.1".to_string(),
|
||||
port: 8888,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_socks5() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "socks5://127.0.0.1:9050").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://127.0.0.1:9050".to_string(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 9050,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,921 +0,0 @@
|
||||
use super::*;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::key::DcKey;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_http() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com:80").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "http://www.hello.com:80".to_string(),
|
||||
host: "www.hello.com".to_string(),
|
||||
port: 80
|
||||
}
|
||||
);
|
||||
|
||||
// If it has no explicit port, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// If it has a path, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com/").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com/".to_string(),
|
||||
}
|
||||
);
|
||||
let qr = check_qr(&ctx.ctx, "http://www.hello.com/hello").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com/hello".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// Test that QR code whitespace is stripped.
|
||||
// Users can copy-paste QR code contents and "scan"
|
||||
// from the clipboard.
|
||||
let qr = check_qr(&ctx.ctx, " \thttp://www.hello.com/hello \n\t \r\n ").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "http://www.hello.com/hello".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_https() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com:443").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "https://www.hello.com:443".to_string(),
|
||||
host: "www.hello.com".to_string(),
|
||||
port: 443
|
||||
}
|
||||
);
|
||||
|
||||
// If it has no explicit port, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://www.hello.com".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// If it has a path, then it is not a proxy.
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com/").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://www.hello.com/".to_string(),
|
||||
}
|
||||
);
|
||||
let qr = check_qr(&ctx.ctx, "https://www.hello.com/hello").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://www.hello.com/hello".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_text() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "I am so cool").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Text {
|
||||
text: "I am so cool".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_vcard() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"BEGIN:VCARD\nVERSION:3.0\nN:Last;First\nEMAIL;TYPE=INTERNET:stress@test.local\nEND:VCARD",
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Qr::Addr { contact_id, draft } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||
assert_eq!(contact.get_name(), "First Last");
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_display_name(), "First Last");
|
||||
assert!(draft.is_none());
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_matmsg() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"MATMSG:TO:\n\nstress@test.local ; \n\nSUB:\n\nSubject here\n\nBODY:\n\nhelloworld\n;;",
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Qr::Addr { contact_id, draft } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||
assert!(draft.is_none());
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_mailto() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"mailto:stress@test.local?subject=hello&body=beautiful+world",
|
||||
)
|
||||
.await?;
|
||||
if let Qr::Addr { contact_id, draft } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||
assert_eq!(draft.unwrap(), "hello\nbeautiful world");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
let res = check_qr(&ctx.ctx, "mailto:no-questionmark@example.org").await?;
|
||||
if let Qr::Addr { contact_id, draft } = res {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "no-questionmark@example.org");
|
||||
assert!(draft.is_none());
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
let res = check_qr(&ctx.ctx, "mailto:no-addr").await;
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_smtp() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
if let Qr::Addr { contact_id, draft } =
|
||||
check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await?
|
||||
{
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "stress@test.local");
|
||||
assert!(draft.is_none());
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_ideltachat_link() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"https://i.delta.chat/#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
).await?;
|
||||
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"https://i.delta.chat#79252762C34C5096AF57958F4FC3D21A81B0F0A7&a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
).await?;
|
||||
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too.
|
||||
// see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_tolerance_for_issue_1969() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7%23a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
).await?;
|
||||
|
||||
assert!(matches!(qr, Qr::AskVerifyGroup { .. }));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_group() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
).await?;
|
||||
if let Qr::AskVerifyGroup {
|
||||
contact_id,
|
||||
grpname,
|
||||
..
|
||||
} = qr
|
||||
{
|
||||
assert_ne!(contact_id, ContactId::UNDEFINED);
|
||||
assert_eq!(grpname, "test ? test !");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
let ctx = TestContext::new().await;
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL9cxRL"
|
||||
).await?;
|
||||
if let Qr::AskVerifyGroup {
|
||||
contact_id,
|
||||
grpname,
|
||||
..
|
||||
} = qr
|
||||
{
|
||||
assert_ne!(contact_id, ContactId::UNDEFINED);
|
||||
assert_eq!(grpname, "test ? test !");
|
||||
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_invalid_token() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
// Token cannot contain "/"
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL/cxRL"
|
||||
).await?;
|
||||
|
||||
assert!(matches!(qr, Qr::FprMismatch { .. }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_secure_join() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||
).await?;
|
||||
|
||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||
assert_ne!(contact_id, ContactId::UNDEFINED);
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=J%C3%B6rn%20P.+P.&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||
).await?;
|
||||
|
||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||
assert_eq!(contact.get_authname(), "Jörn P. P.");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
// Regression test
|
||||
let ctx = TestContext::new().await;
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&n=&i=TbnwJ6lSvD5&s=0ejvbdFSQxB"
|
||||
).await?;
|
||||
|
||||
if let Qr::AskVerifyContact { contact_id, .. } = qr {
|
||||
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
|
||||
assert_eq!(contact.get_addr(), "cli@deltachat.de");
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let alice_contact_id = Contact::create(&ctx, "Alice", "alice@example.org")
|
||||
.await
|
||||
.context("failed to create contact")?;
|
||||
let pub_key = alice_keypair().public;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.org".to_string(),
|
||||
last_seen: 1,
|
||||
last_seen_autocrypt: 1,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(pub_key.clone()),
|
||||
public_key_fingerprint: Some(pub_key.dc_fingerprint()),
|
||||
gossip_key: None,
|
||||
gossip_timestamp: 0,
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
backward_verified_key_id: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
assert!(
|
||||
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
|
||||
"failed to save peerstate"
|
||||
);
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprMismatch { contact_id, .. } = qr {
|
||||
assert_eq!(contact_id, Some(alice_contact_id));
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
&format!(
|
||||
"OPENPGP4FPR:{}#a=alice@example.org",
|
||||
pub_key.dc_fingerprint()
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprOk { contact_id, .. } = qr {
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org",
|
||||
)
|
||||
.await?,
|
||||
Qr::FprMismatch { contact_id: None }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_without_addr() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::FprWithoutAddr {
|
||||
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::FprWithoutAddr {
|
||||
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await;
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_withdraw_verifycontact() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let qr = get_securejoin_qr(&alice, None).await?;
|
||||
|
||||
// scanning own verify-contact code offers withdrawing
|
||||
assert!(matches!(
|
||||
check_qr(&alice, &qr).await?,
|
||||
Qr::WithdrawVerifyContact { .. }
|
||||
));
|
||||
set_config_from_qr(&alice, &qr).await?;
|
||||
|
||||
// scanning withdrawn verify-contact code offers reviving
|
||||
assert!(matches!(
|
||||
check_qr(&alice, &qr).await?,
|
||||
Qr::ReviveVerifyContact { .. }
|
||||
));
|
||||
set_config_from_qr(&alice, &qr).await?;
|
||||
assert!(matches!(
|
||||
check_qr(&alice, &qr).await?,
|
||||
Qr::WithdrawVerifyContact { .. }
|
||||
));
|
||||
|
||||
// someone else always scans as ask-verify-contact
|
||||
let bob = TestContext::new_bob().await;
|
||||
assert!(matches!(
|
||||
check_qr(&bob, &qr).await?,
|
||||
Qr::AskVerifyContact { .. }
|
||||
));
|
||||
assert!(set_config_from_qr(&bob, &qr).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_withdraw_verifygroup() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let qr = get_securejoin_qr(&alice, Some(chat_id)).await?;
|
||||
|
||||
// scanning own verify-group code offers withdrawing
|
||||
if let Qr::WithdrawVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? {
|
||||
assert_eq!(grpname, "foo");
|
||||
} else {
|
||||
bail!("Wrong QR type, expected WithdrawVerifyGroup");
|
||||
}
|
||||
set_config_from_qr(&alice, &qr).await?;
|
||||
|
||||
// scanning withdrawn verify-group code offers reviving
|
||||
if let Qr::ReviveVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? {
|
||||
assert_eq!(grpname, "foo");
|
||||
} else {
|
||||
bail!("Wrong QR type, expected ReviveVerifyGroup");
|
||||
}
|
||||
|
||||
// someone else always scans as ask-verify-group
|
||||
let bob = TestContext::new_bob().await;
|
||||
if let Qr::AskVerifyGroup { grpname, .. } = check_qr(&bob, &qr).await? {
|
||||
assert_eq!(grpname, "foo");
|
||||
} else {
|
||||
bail!("Wrong QR type, expected AskVerifyGroup");
|
||||
}
|
||||
assert!(set_config_from_qr(&bob, &qr).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_and_apply_dclogin() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let result = check_qr(&ctx.ctx, "dclogin:usename+extension@host?p=1234&v=1").await?;
|
||||
if let Qr::Login { address, options } = result {
|
||||
assert_eq!(address, "usename+extension@host".to_owned());
|
||||
|
||||
if let LoginOptions::V1 { mail_pw, .. } = options {
|
||||
assert_eq!(mail_pw, "1234".to_owned());
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
} else {
|
||||
bail!("wrong type")
|
||||
}
|
||||
|
||||
assert!(ctx.ctx.get_config(Config::Addr).await?.is_none());
|
||||
assert!(ctx.ctx.get_config(Config::MailPw).await?.is_none());
|
||||
|
||||
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&v=1").await?;
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::Addr).await?,
|
||||
Some("username+extension@host".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailPw).await?,
|
||||
Some("1234".to_owned())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_and_apply_dclogin_advanced_options() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
set_config_from_qr(&ctx.ctx, "dclogin:username+extension@host?p=1234&spw=4321&sh=send.host&sp=7273&su=SendUser&ih=host.tld&ip=4343&iu=user&ipw=password&is=ssl&ic=1&sc=3&ss=plain&v=1").await?;
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::Addr).await?,
|
||||
Some("username+extension@host".to_owned())
|
||||
);
|
||||
|
||||
// `p=1234` is ignored, because `ipw=password` is set
|
||||
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailServer).await?,
|
||||
Some("host.tld".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailPort).await?,
|
||||
Some("4343".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailUser).await?,
|
||||
Some("user".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailPw).await?,
|
||||
Some("password".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::MailSecurity).await?,
|
||||
Some("1".to_owned()) // ssl
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::ImapCertificateChecks).await?,
|
||||
Some("1".to_owned())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendPw).await?,
|
||||
Some("4321".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendServer).await?,
|
||||
Some("send.host".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendPort).await?,
|
||||
Some("7273".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendUser).await?,
|
||||
Some("SendUser".to_owned())
|
||||
);
|
||||
|
||||
// `sc` option is actually ignored and `ic` is used instead
|
||||
// because `smtp_certificate_checks` is deprecated.
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SmtpCertificateChecks).await?,
|
||||
Some("1".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::SendSecurity).await?,
|
||||
Some("3".to_owned()) // plain
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_account() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Account {
|
||||
domain: "example.org".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
// Test it again with lowercased "dcaccount:" uri scheme
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Account {
|
||||
domain: "example.org".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_webrtc_instance() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://basicurl.com/$ROOM").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::WebrtcInstance {
|
||||
domain: "basicurl.com".to_string(),
|
||||
instance_pattern: "basicwebrtc:https://basicurl.com/$ROOM".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
// Test it again with mixcased "dcWebRTC:" uri scheme
|
||||
let qr = check_qr(&ctx.ctx, "dcWebRTC:https://example.org/").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::WebrtcInstance {
|
||||
domain: "example.org".to_string(),
|
||||
instance_pattern: "https://example.org/".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_tg_socks_proxy() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://84.53.239.95:4145".to_string(),
|
||||
host: "84.53.239.95".to_string(),
|
||||
port: 4145,
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://foo.bar:123".to_string(),
|
||||
host: "foo.bar".to_string(),
|
||||
port: 123,
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://foo.baz:1080".to_string(),
|
||||
host: "foo.baz".to_string(),
|
||||
port: 1080,
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(
|
||||
&t,
|
||||
"https://t.me/socks?server=foo.baz&port=12345&user=ada&pass=ms%21%2F%24",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://ada:ms%21%2F%24@foo.baz:12345".to_string(),
|
||||
host: "foo.baz".to_string(),
|
||||
port: 12345,
|
||||
}
|
||||
);
|
||||
|
||||
// wrong domain results in Qr:Url instead of Qr::Socks5Proxy
|
||||
let qr = check_qr(&t, "https://not.me/socks?noserver=84.53.239.95&port=4145").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://not.me/socks?noserver=84.53.239.95&port=4145".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?noserver=84.53.239.95&port=4145").await;
|
||||
assert!(qr.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_account_bad_scheme() {
|
||||
let ctx = TestContext::new().await;
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"DCACCOUNT:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
|
||||
// Test it again with lowercased "dcaccount:" uri scheme
|
||||
let res = check_qr(
|
||||
&ctx.ctx,
|
||||
"dcaccount:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_webrtc_instance_config_from_qr() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
|
||||
assert!(res.is_err());
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
||||
|
||||
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
|
||||
"https://example.org/"
|
||||
);
|
||||
|
||||
let res =
|
||||
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
|
||||
"basicwebrtc:https://foo.bar/?$ROOM&test"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_proxy_config_from_qr() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false);
|
||||
|
||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some("socks5://foo:666".to_string())
|
||||
);
|
||||
|
||||
// Test URL without port.
|
||||
//
|
||||
// Also check that whitespace is trimmed.
|
||||
let res = set_config_from_qr(&t, " https://t.me/socks?server=1.2.3.4\n").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some("socks5://1.2.3.4:1080\nsocks5://foo:666".to_string())
|
||||
);
|
||||
|
||||
// make sure, user&password are set when specified in the URL
|
||||
// Password is an URL-encoded "x&%$X".
|
||||
let res =
|
||||
set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"socks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080\nsocks5://foo:666".to_string()
|
||||
)
|
||||
);
|
||||
|
||||
// Scanning existing proxy brings it to the top in the list.
|
||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"socks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080".to_string()
|
||||
)
|
||||
);
|
||||
|
||||
set_config_from_qr(
|
||||
&t,
|
||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
// SOCKS5 config does not have port 1080 explicitly specified,
|
||||
// but should bring `socks5://1.2.3.4:1080` to the top instead of creating another entry.
|
||||
set_config_from_qr(&t, "socks5://1.2.3.4").await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::ProxyUrl).await?,
|
||||
Some(
|
||||
"socks5://1.2.3.4:1080\nss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1\nsocks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_shadowsocks() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1".to_string(),
|
||||
host: "192.168.100.1".to_string(),
|
||||
port: 8888,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_socks5() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx.ctx, "socks5://127.0.0.1:9050").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Proxy {
|
||||
url: "socks5://127.0.0.1:9050".to_string(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 9050,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure that `DCBACKUP2` QR code does not fail to deserialize
|
||||
/// because iroh changes the format of `NodeAddr`
|
||||
/// as happened between iroh 0.29 and iroh 0.30 before.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_backup() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&ctx, r#"DCBACKUP2:TWSv6ZjDPa5eoxkocj7xMi8r&{"node_id":"9afc1ea5b4f543e5cdd7b7a21cd26aee7c0b1e1c2af26790896fbd8932a06e1e","relay_url":null,"direct_addresses":["192.168.1.10:12345"]}"#).await?;
|
||||
assert!(matches!(qr, Qr::Backup2 { .. }));
|
||||
|
||||
let qr = check_qr(&ctx, r#"DCBACKUP2:AIvFjRFBt_aMiisSZ8P33JqY&{"node_id":"buzkyd4x76w66qtanjk5fm6ikeuo4quletajowsl3a3p7l6j23pa","info":{"relay_url":null,"direct_addresses":["192.168.1.5:12345"]}}"#).await?;
|
||||
assert!(matches!(qr, Qr::Backup2 { .. }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -187,7 +187,7 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(clippy::assertions_on_constants)]
|
||||
#[allow(clippy::assertions_on_constants)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quota_thresholds() -> anyhow::Result<()> {
|
||||
assert!(QUOTA_ALLCLEAR_PERCENTAGE > 50);
|
||||
|
||||
@@ -285,7 +285,6 @@ pub(crate) async fn set_msg_reaction(
|
||||
&& msg_id.get_state(context).await?.is_outgoing()
|
||||
{
|
||||
context.emit_event(EventType::IncomingReaction {
|
||||
chat_id,
|
||||
contact_id,
|
||||
msg_id,
|
||||
reaction,
|
||||
@@ -583,7 +582,6 @@ Here's my footer -- bob@example.net"
|
||||
|
||||
async fn expect_incoming_reactions_event(
|
||||
t: &TestContext,
|
||||
expected_chat_id: ChatId,
|
||||
expected_msg_id: MsgId,
|
||||
expected_contact_id: ContactId,
|
||||
expected_reaction: &str,
|
||||
@@ -601,12 +599,10 @@ Here's my footer -- bob@example.net"
|
||||
.await;
|
||||
match event {
|
||||
EventType::IncomingReaction {
|
||||
chat_id,
|
||||
msg_id,
|
||||
contact_id,
|
||||
reaction,
|
||||
} => {
|
||||
assert_eq!(chat_id, expected_chat_id);
|
||||
assert_eq!(msg_id, expected_msg_id);
|
||||
assert_eq!(contact_id, expected_contact_id);
|
||||
assert_eq!(reaction, Reaction::from(expected_reaction));
|
||||
@@ -681,14 +677,7 @@ Here's my footer -- bob@example.net"
|
||||
assert_eq!(bob_reaction.as_str(), "👍");
|
||||
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
|
||||
.await?;
|
||||
expect_incoming_reactions_event(
|
||||
&alice,
|
||||
chat_alice.id,
|
||||
alice_msg.sender_msg_id,
|
||||
*bob_id,
|
||||
"👍",
|
||||
)
|
||||
.await?;
|
||||
expect_incoming_reactions_event(&alice, alice_msg.sender_msg_id, *bob_id, "👍").await?;
|
||||
expect_no_unwanted_events(&alice).await;
|
||||
|
||||
// Alice reacts to own message.
|
||||
@@ -731,14 +720,8 @@ Here's my footer -- bob@example.net"
|
||||
send_reaction(&bob, bob_msg1.id, "👍").await?;
|
||||
let bob_send_reaction = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&bob_send_reaction).await;
|
||||
expect_incoming_reactions_event(
|
||||
&alice,
|
||||
alice_chat.id,
|
||||
alice_msg1.sender_msg_id,
|
||||
alice_bob_id,
|
||||
"👍",
|
||||
)
|
||||
.await?;
|
||||
expect_incoming_reactions_event(&alice, alice_msg1.sender_msg_id, alice_bob_id, "👍")
|
||||
.await?;
|
||||
expect_no_unwanted_events(&alice).await;
|
||||
|
||||
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::iter;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line, ContactAddress};
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use mailparse::SingleInfo;
|
||||
@@ -33,9 +33,9 @@ use crate::param::{Param, Params};
|
||||
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::reaction::{set_msg_reaction, Reaction};
|
||||
use crate::rusqlite::OptionalExtension;
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
use crate::simplify;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{self, buf_compress, remove_subject_prefix};
|
||||
@@ -158,7 +158,7 @@ async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId>
|
||||
/// If `is_partial_download` is set, it contains the full message size in bytes.
|
||||
/// Do not confuse that with `replace_msg_id` that will be set when the full message is loaded
|
||||
/// later.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn receive_imf_inner(
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
@@ -696,7 +696,7 @@ pub async fn from_field_to_contact_id(
|
||||
/// Creates a `ReceivedMsg` from given parts which might consist of
|
||||
/// multiple messages (if there are multiple attachments).
|
||||
/// Every entry in `mime_parser.parts` produces a new row in the `msgs` table.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
|
||||
async fn add_parts(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
@@ -1528,9 +1528,6 @@ 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;
|
||||
}
|
||||
(better_msg, Viewtype::Text)
|
||||
} else {
|
||||
(&part.msg, part.typ)
|
||||
@@ -1664,14 +1661,7 @@ RETURNING id
|
||||
// check if any part contains a webxdc topic id
|
||||
if part.typ == Viewtype::Webxdc {
|
||||
if let Some(topic) = mime_parser.get_header(HeaderDef::IrohGossipTopic) {
|
||||
// default encoding of topic ids is `hex`.
|
||||
let mut topic_raw = [0u8; 32];
|
||||
BASE32_NOPAD
|
||||
.decode_mut(topic.to_ascii_uppercase().as_bytes(), &mut topic_raw)
|
||||
.map_err(|e| e.error)
|
||||
.context("Wrong gossip topic header")?;
|
||||
|
||||
let topic = TopicId::from_bytes(topic_raw);
|
||||
let topic = TopicId::from_str(topic).context("wrong gossip topic header")?;
|
||||
insert_topic_stub(context, *msg_id, topic).await?;
|
||||
} else {
|
||||
warn!(context, "webxdc doesn't have a gossip topic")
|
||||
@@ -1850,7 +1840,7 @@ async fn lookup_chat_by_reply(
|
||||
Ok(Some((parent_chat.id, parent_chat.blocked)))
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn lookup_chat_or_create_adhoc_group(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
@@ -1894,20 +1884,10 @@ async fn lookup_chat_or_create_adhoc_group(
|
||||
if !contact_ids.contains(&from_id) {
|
||||
contact_ids.push(from_id);
|
||||
}
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
t.pragma_update(None, "query_only", "0")?;
|
||||
t.execute(
|
||||
"CREATE TEMP TABLE temp.contacts (
|
||||
id INTEGER PRIMARY KEY
|
||||
) STRICT",
|
||||
(),
|
||||
)?;
|
||||
let mut stmt = t.prepare("INSERT INTO temp.contacts(id) VALUES (?)")?;
|
||||
for &id in &contact_ids {
|
||||
stmt.execute((id,))?;
|
||||
}
|
||||
let val = t
|
||||
.query_row(
|
||||
if let Some((chat_id, blocked)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
&format!(
|
||||
"SELECT c.id, c.blocked
|
||||
FROM chats c INNER JOIN msgs m ON c.id=m.chat_id
|
||||
WHERE m.hidden=0 AND c.grpid='' AND c.name=?
|
||||
@@ -1916,22 +1896,24 @@ async fn lookup_chat_or_create_adhoc_group(
|
||||
AND add_timestamp >= remove_timestamp)=?
|
||||
AND (SELECT COUNT(*) FROM chats_contacts
|
||||
WHERE chat_id=c.id
|
||||
AND contact_id NOT IN (SELECT id FROM temp.contacts)
|
||||
AND contact_id NOT IN ({})
|
||||
AND add_timestamp >= remove_timestamp)=0
|
||||
ORDER BY m.timestamp DESC",
|
||||
(&grpname, contact_ids.len()),
|
||||
|row| {
|
||||
let id: ChatId = row.get(0)?;
|
||||
let blocked: Blocked = row.get(1)?;
|
||||
Ok((id, blocked))
|
||||
},
|
||||
)
|
||||
.optional()?;
|
||||
t.execute("DROP TABLE temp.contacts", ())?;
|
||||
Ok(val)
|
||||
};
|
||||
let query_only = true;
|
||||
if let Some((chat_id, blocked)) = context.sql.transaction_ex(query_only, trans_fn).await? {
|
||||
sql::repeat_vars(contact_ids.len()),
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
params_iter(&[&grpname])
|
||||
.chain(params_iter(&[contact_ids.len()]))
|
||||
.chain(params_iter(&contact_ids)),
|
||||
),
|
||||
|row| {
|
||||
let id: ChatId = row.get(0)?;
|
||||
let blocked: Blocked = row.get(1)?;
|
||||
Ok((id, blocked))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
"Assigning message to ad-hoc group {chat_id} with matching name and members."
|
||||
@@ -1995,7 +1977,7 @@ async fn is_probably_private_reply(
|
||||
/// than two members, a new ad hoc group is created.
|
||||
///
|
||||
/// On success the function returns the created (chat_id, chat_blocked) tuple.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_group(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
@@ -2100,14 +2082,13 @@ async fn create_group(
|
||||
}
|
||||
members.extend(to_ids);
|
||||
|
||||
// Add all members with 0 timestamp
|
||||
// because we don't know the real timestamp of their addition.
|
||||
// This will allow other senders who support
|
||||
// `Chat-Group-Member-Timestamps` to overwrite
|
||||
// timestamps later.
|
||||
let timestamp = 0;
|
||||
|
||||
chat::add_to_chat_contacts_table(context, timestamp, new_chat_id, &members).await?;
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
new_chat_id,
|
||||
&members,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
context.emit_event(EventType::ChatModified(new_chat_id));
|
||||
@@ -2208,11 +2189,11 @@ async fn update_chats_contacts_timestamps(
|
||||
/// Apply group member list, name, avatar and protection status changes from the MIME message.
|
||||
///
|
||||
/// Returns `Vec` of group changes messages and, optionally, a better message to replace the
|
||||
/// original system message. If the better message is empty, the original system message
|
||||
/// should be trashed.
|
||||
/// original system message.
|
||||
///
|
||||
/// * `to_ids` - contents of the `To` and `Cc` headers.
|
||||
/// * `past_ids` - contents of the `Chat-Group-Past-Members` header.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn apply_group_changes(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
@@ -2234,6 +2215,7 @@ async fn apply_group_changes(
|
||||
let mut send_event_chat_modified = false;
|
||||
let (mut removed_id, mut added_id) = (None, None);
|
||||
let mut better_msg = None;
|
||||
let mut group_changes_msgs = Vec::new();
|
||||
|
||||
// True if a Delta Chat client has explicitly added our current primary address.
|
||||
let self_added =
|
||||
@@ -2243,7 +2225,7 @@ async fn apply_group_changes(
|
||||
false
|
||||
};
|
||||
|
||||
let chat_contacts =
|
||||
let mut chat_contacts =
|
||||
HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
|
||||
let is_from_in_chat =
|
||||
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
|
||||
@@ -2334,141 +2316,114 @@ async fn apply_group_changes(
|
||||
}
|
||||
}
|
||||
|
||||
if is_from_in_chat {
|
||||
if chat.member_list_is_stale(context).await? {
|
||||
info!(context, "Member list is stale.");
|
||||
let mut new_members: HashSet<ContactId> = HashSet::from_iter(to_ids.iter().copied());
|
||||
new_members.insert(ContactId::SELF);
|
||||
if !from_id.is_special() {
|
||||
new_members.insert(from_id);
|
||||
}
|
||||
// These are for adding info messages about implicit membership changes, so they are only
|
||||
// filled when such messages are needed.
|
||||
let mut added_ids = HashSet::<ContactId>::new();
|
||||
let mut removed_ids = HashSet::<ContactId>::new();
|
||||
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
// Remove all contacts and tombstones.
|
||||
transaction.execute(
|
||||
"DELETE FROM chats_contacts
|
||||
WHERE chat_id=?",
|
||||
(chat_id,),
|
||||
)?;
|
||||
|
||||
// Insert contacts with default timestamps of 0.
|
||||
let mut statement = transaction.prepare(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id)
|
||||
VALUES (?, ?)",
|
||||
)?;
|
||||
for contact_id in &new_members {
|
||||
statement.execute((chat_id, contact_id))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
send_event_chat_modified = true;
|
||||
} else if let Some(ref chat_group_member_timestamps) =
|
||||
mime_parser.chat_group_member_timestamps()
|
||||
{
|
||||
send_event_chat_modified |= update_chats_contacts_timestamps(
|
||||
context,
|
||||
chat_id,
|
||||
Some(from_id),
|
||||
to_ids,
|
||||
past_ids,
|
||||
chat_group_member_timestamps,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
let mut new_members;
|
||||
if self_added {
|
||||
new_members = HashSet::from_iter(to_ids.iter().copied());
|
||||
new_members.insert(ContactId::SELF);
|
||||
if !from_id.is_special() {
|
||||
new_members.insert(from_id);
|
||||
}
|
||||
} else {
|
||||
new_members = chat_contacts.clone();
|
||||
}
|
||||
|
||||
// Allow non-Delta Chat MUAs to add members.
|
||||
if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
|
||||
// Don't delete any members locally, but instead add absent ones to provide group
|
||||
// membership consistency for all members:
|
||||
new_members.extend(to_ids.iter());
|
||||
}
|
||||
|
||||
// Apply explicit addition if any.
|
||||
if let Some(added_id) = added_id {
|
||||
new_members.insert(added_id);
|
||||
}
|
||||
|
||||
// Apply explicit removal if any.
|
||||
if let Some(removed_id) = removed_id {
|
||||
new_members.remove(&removed_id);
|
||||
}
|
||||
|
||||
if new_members != chat_contacts {
|
||||
chat::update_chat_contacts_table(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
chat_id,
|
||||
&new_members,
|
||||
)
|
||||
.await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
if let Some(ref chat_group_member_timestamps) = mime_parser.chat_group_member_timestamps() {
|
||||
send_event_chat_modified |= update_chats_contacts_timestamps(
|
||||
context,
|
||||
chat_id,
|
||||
Some(from_id),
|
||||
to_ids,
|
||||
past_ids,
|
||||
chat_group_member_timestamps,
|
||||
)
|
||||
.await?;
|
||||
let new_chat_contacts = HashSet::<ContactId>::from_iter(
|
||||
chat::get_chat_contacts(context, chat_id)
|
||||
.await?
|
||||
.iter()
|
||||
.copied(),
|
||||
);
|
||||
added_ids = new_chat_contacts
|
||||
.difference(&chat_contacts)
|
||||
.copied()
|
||||
.collect();
|
||||
removed_ids = chat_contacts
|
||||
.difference(&new_chat_contacts)
|
||||
.copied()
|
||||
.collect();
|
||||
} else if is_from_in_chat {
|
||||
let mut new_members = HashSet::from_iter(to_ids.iter().copied());
|
||||
new_members.insert(ContactId::SELF);
|
||||
if !from_id.is_special() {
|
||||
new_members.insert(from_id);
|
||||
}
|
||||
|
||||
chat_id
|
||||
.update_timestamp(
|
||||
if !self_added {
|
||||
if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
|
||||
// Allow non-Delta Chat MUAs to add members.
|
||||
added_ids = new_members.difference(&chat_contacts).copied().collect();
|
||||
}
|
||||
|
||||
if let Some(added_id) = added_id {
|
||||
added_ids.insert(added_id);
|
||||
}
|
||||
new_members.clone_from(&chat_contacts);
|
||||
// Don't delete any members locally, but instead add absent ones to provide group
|
||||
// membership consistency for all members:
|
||||
new_members.extend(added_ids.clone());
|
||||
}
|
||||
if let Some(removed_id) = removed_id {
|
||||
new_members.remove(&removed_id);
|
||||
}
|
||||
|
||||
if new_members != chat_contacts {
|
||||
chat::update_chat_contacts_table(
|
||||
context,
|
||||
Param::MemberListTimestamp,
|
||||
mime_parser.timestamp_sent,
|
||||
chat_id,
|
||||
&new_members,
|
||||
)
|
||||
.await?;
|
||||
chat_contacts = new_members;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
let new_chat_contacts = HashSet::<ContactId>::from_iter(
|
||||
chat::get_chat_contacts(context, chat_id)
|
||||
.await?
|
||||
.iter()
|
||||
.copied(),
|
||||
);
|
||||
|
||||
// These are for adding info messages about implicit membership changes.
|
||||
let mut added_ids: HashSet<ContactId> = new_chat_contacts
|
||||
.difference(&chat_contacts)
|
||||
.copied()
|
||||
.collect();
|
||||
let mut removed_ids: HashSet<ContactId> = chat_contacts
|
||||
.difference(&new_chat_contacts)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
if let Some(added_id) = added_id {
|
||||
if !added_ids.remove(&added_id) && !self_added {
|
||||
// No-op "Member added" message.
|
||||
//
|
||||
// Trash it.
|
||||
better_msg = Some(String::new());
|
||||
}
|
||||
added_ids.remove(&added_id);
|
||||
}
|
||||
if let Some(removed_id) = removed_id {
|
||||
removed_ids.remove(&removed_id);
|
||||
}
|
||||
let group_changes_msgs = if self_added {
|
||||
Vec::new()
|
||||
} else {
|
||||
group_changes_msgs(context, &added_ids, &removed_ids, chat_id).await?
|
||||
};
|
||||
if !added_ids.is_empty() {
|
||||
warn!(
|
||||
context,
|
||||
"Implicit addition of {added_ids:?} to chat {chat_id}."
|
||||
);
|
||||
}
|
||||
if !removed_ids.is_empty() {
|
||||
warn!(
|
||||
context,
|
||||
"Implicit removal of {removed_ids:?} from chat {chat_id}."
|
||||
);
|
||||
}
|
||||
group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
|
||||
for contact_id in added_ids {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
group_changes_msgs.push(
|
||||
stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
for contact_id in removed_ids {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
group_changes_msgs.push(
|
||||
stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
if !new_chat_contacts.contains(&ContactId::SELF) {
|
||||
if !chat_contacts.contains(&ContactId::SELF) {
|
||||
warn!(
|
||||
context,
|
||||
"Received group avatar update for group chat {chat_id} we are not a member of."
|
||||
);
|
||||
} else if !new_chat_contacts.contains(&from_id) {
|
||||
} else if !chat_contacts.contains(&from_id) {
|
||||
warn!(
|
||||
context,
|
||||
"Contact {from_id} attempts to modify group chat {chat_id} avatar without being a member.",
|
||||
@@ -2500,45 +2455,6 @@ async fn apply_group_changes(
|
||||
Ok((group_changes_msgs, better_msg))
|
||||
}
|
||||
|
||||
/// Returns a list of strings that should be shown as info messages, informing about group membership changes.
|
||||
async fn group_changes_msgs(
|
||||
context: &Context,
|
||||
added_ids: &HashSet<ContactId>,
|
||||
removed_ids: &HashSet<ContactId>,
|
||||
chat_id: ChatId,
|
||||
) -> Result<Vec<String>> {
|
||||
let mut group_changes_msgs = Vec::new();
|
||||
if !added_ids.is_empty() {
|
||||
warn!(
|
||||
context,
|
||||
"Implicit addition of {added_ids:?} to chat {chat_id}."
|
||||
);
|
||||
}
|
||||
if !removed_ids.is_empty() {
|
||||
warn!(
|
||||
context,
|
||||
"Implicit removal of {removed_ids:?} from chat {chat_id}."
|
||||
);
|
||||
}
|
||||
group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
|
||||
for contact_id in added_ids {
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
group_changes_msgs.push(
|
||||
stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
for contact_id in removed_ids {
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
group_changes_msgs.push(
|
||||
stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(group_changes_msgs)
|
||||
}
|
||||
|
||||
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
|
||||
|
||||
fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
|
||||
@@ -2948,27 +2864,38 @@ async fn mark_recipients_as_verified(
|
||||
to_ids: Vec<ContactId>,
|
||||
mimeparser: &MimeMessage,
|
||||
) -> Result<()> {
|
||||
if to_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ",
|
||||
sql::repeat_vars(to_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(&to_ids),
|
||||
|row| {
|
||||
let to_addr: String = row.get(0)?;
|
||||
let is_verified: i32 = row.get(1).unwrap_or(0);
|
||||
Ok((to_addr, is_verified != 0))
|
||||
},
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
for id in to_ids {
|
||||
let Some((to_addr, is_verified)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c
|
||||
LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id=?",
|
||||
(id,),
|
||||
|row| {
|
||||
let to_addr: String = row.get(0)?;
|
||||
let is_verified: i32 = row.get(1).unwrap_or(0);
|
||||
Ok((to_addr, is_verified != 0))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for (to_addr, is_verified) in rows {
|
||||
// mark gossiped keys (if any) as verified
|
||||
if let Some(gossiped_key) = mimeparser.gossiped_keys.get(&to_addr.to_lowercase()) {
|
||||
if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? {
|
||||
@@ -3092,4 +3019,4 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod receive_imf_tests;
|
||||
mod tests;
|
||||
|
||||
@@ -1664,12 +1664,8 @@ async fn test_pdf_filename_simple() {
|
||||
assert_eq!(msg.viewtype, Viewtype::File);
|
||||
assert_eq!(msg.text, "mail body");
|
||||
let file_path = msg.param.get(Param::File).unwrap();
|
||||
assert_eq!(
|
||||
file_path,
|
||||
// That's the blake3 hash of the file content:
|
||||
"$BLOBDIR/24a6af459cec5d733374aeaa19a6133.pdf"
|
||||
);
|
||||
assert_eq!(msg.param.get(Param::Filename).unwrap(), "simple.pdf");
|
||||
assert!(file_path.starts_with("$BLOBDIR/simple"));
|
||||
assert!(file_path.ends_with(".pdf"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1684,8 +1680,8 @@ async fn test_pdf_filename_continuation() {
|
||||
assert_eq!(msg.viewtype, Viewtype::File);
|
||||
assert_eq!(msg.text, "mail body");
|
||||
let file_path = msg.param.get(Param::File).unwrap();
|
||||
assert!(file_path.starts_with("$BLOBDIR/"));
|
||||
assert_eq!(msg.get_filename().unwrap(), "test pdf äöüß.pdf");
|
||||
assert!(file_path.starts_with("$BLOBDIR/test pdf äöüß"));
|
||||
assert!(file_path.ends_with(".pdf"));
|
||||
}
|
||||
|
||||
/// HTML-images may come with many embedded images, eg. tiny icons, corners for formatting,
|
||||
@@ -3248,11 +3244,11 @@ async fn test_weird_and_duplicated_filenames() -> Result<()> {
|
||||
"a. tar.tar.gz",
|
||||
] {
|
||||
let attachment = alice.blobdir.join(filename_sent);
|
||||
let content = "File content of tar.gz archive".to_string();
|
||||
let content = format!("File content of {filename_sent}");
|
||||
tokio::fs::write(&attachment, content.as_bytes()).await?;
|
||||
|
||||
let mut msg_alice = Message::new(Viewtype::File);
|
||||
msg_alice.set_file_and_deduplicate(&alice, &attachment, None, None)?;
|
||||
msg_alice.set_file(attachment.to_str().unwrap(), None);
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_msg(alice_chat.id, &mut msg_alice).await;
|
||||
println!("{}", sent.payload());
|
||||
@@ -3266,10 +3262,9 @@ async fn test_weird_and_duplicated_filenames() -> Result<()> {
|
||||
let path = msg.get_file(t).unwrap();
|
||||
let path2 = path.with_file_name("saved.txt");
|
||||
msg.save_file(t, &path2).await.unwrap();
|
||||
assert_eq!(
|
||||
path.file_name().unwrap().to_str().unwrap(),
|
||||
"79402cb76f44c5761888f9036992a76.gz",
|
||||
"The hash of the content should always be the same"
|
||||
assert!(
|
||||
path.to_str().unwrap().ends_with(".tar.gz"),
|
||||
"path {path:?} doesn't end with .tar.gz"
|
||||
);
|
||||
assert_eq!(fs::read_to_string(&path).await.unwrap(), content);
|
||||
assert_eq!(fs::read_to_string(&path2).await.unwrap(), content);
|
||||
@@ -4328,50 +4323,35 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> {
|
||||
async fn test_mua_cant_remove() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let now = time();
|
||||
|
||||
// Alice creates chat with 3 contacts
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(now - 2000, 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
let msg = receive_imf(
|
||||
&alice,
|
||||
format!(
|
||||
"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\
|
||||
From: alice@example.org\r\n\
|
||||
To: <bob@example.net>, <claire@example.org>, <fiona@example.org> \r\n\
|
||||
Date: {date}\r\n\
|
||||
Message-ID: <Mr.alices_original_mail@example.org>\r\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
\r\n\
|
||||
tst\r\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\
|
||||
From: alice@example.org\r\n\
|
||||
To: <bob@example.net>, <claire@example.org>, <fiona@example.org> \r\n\
|
||||
Date: Mon, 12 Dec 2022 14:30:39 +0000\r\n\
|
||||
Message-ID: <Mr.alices_original_mail@example.org>\r\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
\r\n\
|
||||
tst\r\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
let alice_chat = Chat::load_from_db(&alice, msg.chat_id).await?;
|
||||
assert_eq!(alice_chat.typ, Chattype::Group);
|
||||
assert_eq!(alice_chat.member_list_is_stale(&alice).await?, false);
|
||||
|
||||
// Bob uses a classical MUA to answer, removing a recipient.
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(now - 1000, 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
let bob_removes = receive_imf(
|
||||
&alice,
|
||||
format!(
|
||||
"Subject: Re: Message from alice\r\n\
|
||||
From: <bob@example.net>\r\n\
|
||||
To: <alice@example.org>, <claire@example.org>\r\n\
|
||||
Date: {date}\r\n\
|
||||
Message-ID: <bobs_answer_to_two_recipients@example.net>\r\n\
|
||||
In-Reply-To: <Mr.alices_original_mail@example.org>\r\n\
|
||||
\r\n\
|
||||
Hi back!\r\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
b"Subject: Re: Message from alice\r\n\
|
||||
From: <bob@example.net>\r\n\
|
||||
To: <alice@example.org>, <claire@example.org>\r\n\
|
||||
Date: Mon, 12 Dec 2022 14:32:39 +0000\r\n\
|
||||
Message-ID: <bobs_answer_to_two_recipients@example.net>\r\n\
|
||||
In-Reply-To: <Mr.alices_original_mail@example.org>\r\n\
|
||||
\r\n\
|
||||
Hi back!\r\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
@@ -4379,29 +4359,22 @@ async fn test_mua_cant_remove() -> Result<()> {
|
||||
assert_eq!(bob_removes.chat_id, alice_chat.id);
|
||||
let group_chat = Chat::load_from_db(&alice, bob_removes.chat_id).await?;
|
||||
assert_eq!(group_chat.typ, Chattype::Group);
|
||||
assert_eq!(group_chat.member_list_is_stale(&alice).await?, false);
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
|
||||
4
|
||||
);
|
||||
|
||||
// But if the parent message is missing, the message must goto a new ad-hoc group.
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(now, 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
let bob_removes = receive_imf(
|
||||
&alice,
|
||||
format!(
|
||||
"Subject: Re: Message from alice\r\n\
|
||||
From: <bob@example.net>\r\n\
|
||||
To: <alice@example.org>, <claire@example.org>\r\n\
|
||||
Date: {date}\r\n\
|
||||
Message-ID: <bobs_answer_to_two_recipients_1@example.net>\r\n\
|
||||
In-Reply-To: <Mr.missing@example.org>\r\n\
|
||||
\r\n\
|
||||
Hi back!\r\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
b"Subject: Re: Message from alice\r\n\
|
||||
From: <bob@example.net>\r\n\
|
||||
To: <alice@example.org>, <claire@example.org>\r\n\
|
||||
Date: Mon, 12 Dec 2022 14:32:40 +0000\r\n\
|
||||
Message-ID: <bobs_answer_to_two_recipients_1@example.net>\r\n\
|
||||
In-Reply-To: <Mr.missing@example.org>\r\n\
|
||||
\r\n\
|
||||
Hi back!\r\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
@@ -4420,51 +4393,39 @@ async fn test_mua_cant_remove() -> Result<()> {
|
||||
async fn test_mua_can_add() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let now = time();
|
||||
|
||||
// Alice creates chat with 3 contacts
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(now - 2000, 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
let msg = receive_imf(
|
||||
&alice,
|
||||
format!(
|
||||
"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\
|
||||
From: alice@example.org\r\n\
|
||||
To: <bob@example.net>, <claire@example.org>, <fiona@example.org> \r\n\
|
||||
Date: {date}\r\n\
|
||||
Message-ID: <Mr.alices_original_mail@example.org>\r\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
\r\n\
|
||||
Hi!\r\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\
|
||||
From: alice@example.org\r\n\
|
||||
To: <bob@example.net>, <claire@example.org>, <fiona@example.org> \r\n\
|
||||
Date: Mon, 12 Dec 2022 14:30:39 +0000\r\n\
|
||||
Message-ID: <Mr.alices_original_mail@example.org>\r\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
\r\n\
|
||||
Hi!\r\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
let alice_chat = Chat::load_from_db(&alice, msg.chat_id).await?;
|
||||
assert_eq!(alice_chat.typ, Chattype::Group);
|
||||
assert_eq!(alice_chat.member_list_is_stale(&alice).await?, false);
|
||||
|
||||
// Bob uses a classical MUA to answer, adding a recipient.
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(now - 1000, 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
let bob_adds = receive_imf(
|
||||
&alice,
|
||||
format!("Subject: Re: Message from alice\r\n\
|
||||
From: <bob@example.net>\r\n\
|
||||
To: <alice@example.org>, <claire@example.org>, <fiona@example.org>, <greg@example.host>\r\n\
|
||||
Date: {date}\r\n\
|
||||
Message-ID: <bobs_answer_to_two_recipients@example.net>\r\n\
|
||||
In-Reply-To: <Mr.alices_original_mail@example.org>\r\n\
|
||||
\r\n\
|
||||
Hi back!\r\n").as_bytes(),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
&alice,
|
||||
b"Subject: Re: Message from alice\r\n\
|
||||
From: <bob@example.net>\r\n\
|
||||
To: <alice@example.org>, <claire@example.org>, <fiona@example.org>, <greg@example.host>\r\n\
|
||||
Date: Mon, 12 Dec 2022 14:32:39 +0000\r\n\
|
||||
Message-ID: <bobs_answer_to_two_recipients@example.net>\r\n\
|
||||
In-Reply-To: <Mr.alices_original_mail@example.org>\r\n\
|
||||
\r\n\
|
||||
Hi back!\r\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
let group_chat = Chat::load_from_db(&alice, bob_adds.chat_id).await?;
|
||||
assert_eq!(group_chat.typ, Chattype::Group);
|
||||
@@ -4739,7 +4700,8 @@ async fn test_create_group_with_big_msg() -> Result<()> {
|
||||
let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?;
|
||||
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)
|
||||
.await?;
|
||||
let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
|
||||
@@ -4775,7 +4737,8 @@ async fn test_create_group_with_big_msg() -> Result<()> {
|
||||
let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group1").await?;
|
||||
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)?;
|
||||
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)
|
||||
.await?;
|
||||
let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
@@ -5086,32 +5049,6 @@ async fn test_unarchive_on_member_removal() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_op_member_added_is_trash() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "foos", &[bob])
|
||||
.await;
|
||||
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let msg = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&msg).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
let fiona_id = Contact::create(alice, "", "fiona@example.net").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
let msg = alice.pop_sent_msg().await;
|
||||
|
||||
let fiona_id = Contact::create(bob, "", "fiona@example.net").await?;
|
||||
add_contact_to_chat(bob, bob_chat_id, fiona_id).await?;
|
||||
bob.recv_msg_trash(&msg).await;
|
||||
let contacts = get_chat_contacts(bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forged_from() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -5216,7 +5153,8 @@ async fn test_prefer_references_to_downloaded_msgs() -> Result<()> {
|
||||
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.gif");
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_from_bytes(alice, "file", file_bytes, None)?;
|
||||
msg.set_file_from_bytes(alice, "file", file_bytes, None)
|
||||
.await?;
|
||||
let mut sent = alice.send_msg(alice_chat_id, &mut msg).await;
|
||||
sent.payload = sent
|
||||
.payload
|
||||
@@ -5228,7 +5166,8 @@ async fn test_prefer_references_to_downloaded_msgs() -> Result<()> {
|
||||
assert_eq!(received.chat_id, bob.get_chat(alice).await.id);
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_from_bytes(alice, "file", file_bytes, None)?;
|
||||
msg.set_file_from_bytes(alice, "file", file_bytes, None)
|
||||
.await?;
|
||||
let sent = alice.send_msg(alice_chat_id, &mut msg).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.download_state, DownloadState::Available);
|
||||
@@ -5287,6 +5226,7 @@ async fn test_receive_vcard() -> Result<()> {
|
||||
.as_bytes(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_bob_chat = alice.create_chat(bob).await;
|
||||
@@ -5485,100 +5425,3 @@ async fn test_prefer_chat_group_id_over_references() -> Result<()> {
|
||||
assert_ne!(chat1.id, chat2.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that if member timestamps are unknown
|
||||
/// because of the missing `Chat-Group-Member-Timestamps` header,
|
||||
/// then timestamps default to zero.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_default_member_timestamps_to_zero() -> Result<()> {
|
||||
let bob = &TestContext::new_bob().await;
|
||||
|
||||
let now = time();
|
||||
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(now - 1000, 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
let msg = receive_imf(
|
||||
bob,
|
||||
format!(
|
||||
"Subject: Some group\r\n\
|
||||
From: <alice@example.org>\r\n\
|
||||
To: <bob@example.net>, <claire@example.org>, <fiona@example.org>\r\n\
|
||||
Date: {date}\r\n\
|
||||
Message-ID: <first@localhost>\r\n\
|
||||
Chat-Group-ID: foobarbaz12\n\
|
||||
Chat-Group-Name: foo\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
\r\n\
|
||||
Hi!\r\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
let chat = Chat::load_from_db(bob, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat::get_chat_contacts(bob, chat.id).await?.len(), 4);
|
||||
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(now, 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
receive_imf(
|
||||
bob,
|
||||
format!(
|
||||
"Subject: Some group\r\n\
|
||||
From: <claire@example.org>\r\n\
|
||||
To: <alice@example.org>, <bob@example.net>\r\n\
|
||||
Chat-Group-Past-Members: <fiona@example.org>\r\n\
|
||||
Chat-Group-Member-Timestamps: 1737783000 1737783100 1737783200\r\n\
|
||||
Chat-Group-ID: foobarbaz12\n\
|
||||
Chat-Group-Name: foo\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
Date: {date}\r\n\
|
||||
Message-ID: <second@localhost>\r\n\
|
||||
\r\n\
|
||||
Hi back!\r\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
let chat = Chat::load_from_db(bob, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat::get_chat_contacts(bob, chat.id).await?.len(), 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for the bug
|
||||
/// that resulted in an info message
|
||||
/// about Bob addition to the group on Fiona's device.
|
||||
///
|
||||
/// There should be no info messages about implicit
|
||||
/// member changes when we are added to the group.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_two_group_securejoins() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let group_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
|
||||
|
||||
let qr = get_securejoin_qr(alice, Some(group_id)).await?;
|
||||
|
||||
// Bob joins using QR code.
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
|
||||
// Fiona joins using QR code.
|
||||
tcm.exec_securejoin_qr(fiona, alice, &qr).await;
|
||||
|
||||
let fiona_chat_id = fiona.get_last_msg().await.chat_id;
|
||||
fiona
|
||||
.golden_test_chat(fiona_chat_id, "two_group_securejoins")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -671,10 +671,7 @@ async fn fetch_idle(
|
||||
return Ok(session);
|
||||
}
|
||||
|
||||
info!(
|
||||
ctx,
|
||||
"IMAP session in folder {watch_folder:?} supports IDLE, using it."
|
||||
);
|
||||
info!(ctx, "IMAP session supports IDLE, using it.");
|
||||
let session = session
|
||||
.idle(
|
||||
ctx,
|
||||
|
||||
@@ -744,4 +744,850 @@ fn encrypted_and_signed(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod securejoin_tests;
|
||||
mod tests {
|
||||
use deltachat_contact_tools::{ContactAddress, EmailAddress};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{remove_contact_from_chat, CantSendReason};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::{self, Chattype};
|
||||
use crate::imex::{imex, ImexMode};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, chat_protection_enabled};
|
||||
use crate::test_utils::{get_chat_msg, TimeShiftFalsePositiveNote};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum SetupContactCase {
|
||||
Normal,
|
||||
CheckProtectionTimestamp,
|
||||
WrongAliceGossip,
|
||||
SecurejoinWaitTimeout,
|
||||
AliceIsBot,
|
||||
AliceHasName,
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact() {
|
||||
test_setup_contact_ex(SetupContactCase::Normal).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_protection_timestamp() {
|
||||
test_setup_contact_ex(SetupContactCase::CheckProtectionTimestamp).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_wrong_alice_gossip() {
|
||||
test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_wait_timeout() {
|
||||
test_setup_contact_ex(SetupContactCase::SecurejoinWaitTimeout).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_alice_is_bot() {
|
||||
test_setup_contact_ex(SetupContactCase::AliceIsBot).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_alice_has_name() {
|
||||
test_setup_contact_ex(SetupContactCase::AliceHasName).await
|
||||
}
|
||||
|
||||
async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
let _n = TimeShiftFalsePositiveNote;
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
|
||||
if case == SetupContactCase::AliceHasName {
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let bob = tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
||||
.await
|
||||
.unwrap();
|
||||
let alice_auto_submitted_hdr;
|
||||
match case {
|
||||
SetupContactCase::AliceIsBot => {
|
||||
alice.set_config_bool(Config::Bot, true).await.unwrap();
|
||||
alice_auto_submitted_hdr = "Auto-Submitted: auto-generated";
|
||||
}
|
||||
_ => alice_auto_submitted_hdr = "Auto-Submitted: auto-replied",
|
||||
};
|
||||
for t in [&alice, &bob] {
|
||||
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
0
|
||||
);
|
||||
|
||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||
// We want Bob to learn Alice's name from their messages, not from the QR code.
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice Exampleorg"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 2: Bob scans QR-code, sends vc-request
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
|
||||
// Step 3: Alice receives vc-request, sends vc-auth-required
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
||||
assert!(!sent.payload.contains("Alice Exampleorg"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-auth-required"
|
||||
);
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false);
|
||||
assert_eq!(
|
||||
bob_chat.why_cant_send(&bob).await.unwrap(),
|
||||
Some(CantSendReason::SecurejoinWait)
|
||||
);
|
||||
if case == SetupContactCase::SecurejoinWaitTimeout {
|
||||
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT));
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
}
|
||||
|
||||
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Check Bob sent the right message.
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
let mut msg = alice.parse_msg(&sent).await;
|
||||
let vc_request_with_auth_ts_sent = msg
|
||||
.get_header(HeaderDef::Date)
|
||||
.and_then(|value| mailparse::dateparse(value).ok())
|
||||
.unwrap();
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.dc_fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
);
|
||||
|
||||
if case == SetupContactCase::WrongAliceGossip {
|
||||
let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
|
||||
let alice_pubkey = msg
|
||||
.gossiped_keys
|
||||
.insert(alice_addr.to_string(), wrong_pubkey)
|
||||
.unwrap();
|
||||
let contact_bob = alice.add_or_lookup_contact(&bob).await;
|
||||
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
|
||||
|
||||
msg.gossiped_keys
|
||||
.insert(alice_addr.to_string(), alice_pubkey)
|
||||
.unwrap();
|
||||
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||
assert!(contact_bob.is_verified(&alice.ctx).await.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob_id =
|
||||
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
|
||||
assert_eq!(contact_bob.get_authname(), "");
|
||||
|
||||
if case == SetupContactCase::CheckProtectionTimestamp {
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
}
|
||||
|
||||
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
|
||||
assert!(contact_bob.get_name().is_empty());
|
||||
assert_eq!(contact_bob.is_bot(), false);
|
||||
|
||||
// exactly one one-to-one chat should be visible for both now
|
||||
// (check this before calling alice.create_chat() explicitly below)
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
|
||||
// Check Alice got the verified message in her 1:1 chat.
|
||||
{
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
if case == SetupContactCase::CheckProtectionTimestamp {
|
||||
assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure Alice hasn't yet sent their name to Bob.
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
match case {
|
||||
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
|
||||
_ => assert_eq!(contact_alice.get_authname(), ""),
|
||||
};
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
||||
assert!(!sent.payload.contains("Alice Exampleorg"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-contact-confirm"
|
||||
);
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), false);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_alice.get_authname(), "Alice Exampleorg");
|
||||
assert!(contact_alice.get_name().is_empty());
|
||||
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
|
||||
|
||||
if case != SetupContactCase::SecurejoinWaitTimeout {
|
||||
// Later we check that the timeout message isn't added to the already protected chat.
|
||||
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT + 1));
|
||||
assert_eq!(
|
||||
bob_chat
|
||||
.check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||
.await
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// Check Bob got expected info messages in his 1:1 chat.
|
||||
let msg_cnt: usize = match case {
|
||||
SetupContactCase::SecurejoinWaitTimeout => 3,
|
||||
_ => 2,
|
||||
};
|
||||
let mut i = 0..msg_cnt;
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
||||
if case == SetupContactCase::SecurejoinWaitTimeout {
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(
|
||||
msg.get_text(),
|
||||
stock_str::securejoin_wait_timeout(&bob).await
|
||||
);
|
||||
}
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_bad_qr() {
|
||||
let bob = TestContext::new_bob().await;
|
||||
let ret = join_securejoin(&bob.ctx, "not a qr code").await;
|
||||
assert!(ret.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Ensure Bob knows Alice_FP
|
||||
let alice_pubkey = load_self_public_key(&alice.ctx).await?;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.org".into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 10,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(alice_pubkey.clone()),
|
||||
public_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
||||
gossip_key: Some(alice_pubkey.clone()),
|
||||
gossip_timestamp: 10,
|
||||
gossip_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
backward_verified_key_id: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
peerstate.save_to_db(&bob.ctx.sql).await?;
|
||||
|
||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await?;
|
||||
|
||||
// Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Check Bob sent the right handshake message.
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
||||
&alice.ctx,
|
||||
"",
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
||||
|
||||
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-contact-confirm"
|
||||
);
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
||||
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_concurrent_calls() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// do a scan that is not working as claire is never responding
|
||||
let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012";
|
||||
let claire_id = join_securejoin(&bob, qr_stale).await?;
|
||||
let chat = Chat::load_from_db(&bob, claire_id).await?;
|
||||
assert!(!claire_id.is_special());
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert!(bob.pop_sent_msg().await.payload().contains("claire@foo.de"));
|
||||
|
||||
// subsequent scans shall abort existing ones or run concurrently -
|
||||
// but they must not fail as otherwise the whole qr scanning becomes unusable until restart.
|
||||
let qr = get_securejoin_qr(&alice, None).await?;
|
||||
let alice_id = join_securejoin(&bob, &qr).await?;
|
||||
let chat = Chat::load_from_db(&bob, alice_id).await?;
|
||||
assert!(!alice_id.is_special());
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert_ne!(claire_id, alice_id);
|
||||
assert!(bob
|
||||
.pop_sent_msg()
|
||||
.await
|
||||
.payload()
|
||||
.contains("alice@example.org"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_secure_join() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// We start with empty chatlists.
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
|
||||
let alice_chatid =
|
||||
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
|
||||
|
||||
// Step 1: Generate QR-code, secure-join implied by chatid
|
||||
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 2: Bob scans QR-code, sends vg-request
|
||||
let bob_chatid = join_securejoin(&bob.ctx, &qr).await?;
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
sent.recipient(),
|
||||
EmailAddress::new("alice@example.org").unwrap()
|
||||
);
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
|
||||
// Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`,
|
||||
// but it was only used by Alice in `vg-request-with-auth`.
|
||||
// New Delta Chat versions do not use `Secure-Join-Group` header at all
|
||||
// and it is deprecated.
|
||||
// Now `Secure-Join-Group` header
|
||||
// is only sent in `vg-request-with-auth` for compatibility.
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinGroup).is_none());
|
||||
|
||||
// Step 3: Alice receives vg-request, sends vg-auth-required
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vg-auth-required"
|
||||
);
|
||||
|
||||
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Check Bob sent the right handshake message.
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vg-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob_id =
|
||||
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||
.await?
|
||||
.expect("Contact not found");
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
||||
|
||||
// Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vg-member-added"
|
||||
);
|
||||
// Formally this message is auto-submitted, but as the member addition is a result of an
|
||||
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
|
||||
// be strange to have it in "member-added" messages of verified groups only.
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
// This is a two-member group, but Alice must Autocrypt-gossip to her other devices.
|
||||
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
|
||||
|
||||
{
|
||||
// Now Alice's chat with Bob should still be hidden, the verified message should
|
||||
// appear in the group chat.
|
||||
|
||||
let chat = alice.get_chat(&bob).await;
|
||||
assert_eq!(
|
||||
chat.blocked,
|
||||
Blocked::Yes,
|
||||
"Alice's 1:1 chat with Bob is not hidden"
|
||||
);
|
||||
// There should be 3 messages in the chat:
|
||||
// - The ChatProtectionEnabled message
|
||||
// - You added member bob@example.net
|
||||
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
}
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
||||
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
|
||||
|
||||
// Step 7: Bob receives vg-member-added
|
||||
bob.recv_msg(&sent).await;
|
||||
{
|
||||
// Bob has Alice verified, message shows up in the group chat.
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
|
||||
let chat = bob.get_chat(&alice).await;
|
||||
assert_eq!(
|
||||
chat.blocked,
|
||||
Blocked::Yes,
|
||||
"Bob's 1:1 chat with Alice is not hidden"
|
||||
);
|
||||
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid).await.unwrap() {
|
||||
if let chat::ChatItem::Message { msg_id } = item {
|
||||
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
|
||||
let text = msg.get_text();
|
||||
println!("msg {msg_id} text: {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
|
||||
assert!(bob_chat.is_protected());
|
||||
assert!(bob_chat.typ == Chattype::Group);
|
||||
|
||||
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
|
||||
// The one-to-one chats are used internally for the hidden handshake messages,
|
||||
// however, should not be visible in the UIs.
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
|
||||
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
|
||||
let bobs_chat_with_alice = bob.create_chat(&alice).await;
|
||||
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_no_qr() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let mime = br#"Subject: First thread
|
||||
Message-ID: first@example.org
|
||||
To: Alice <alice@example.org>, Bob <bob@example.net>
|
||||
From: Claire <claire@example.org>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
First thread."#;
|
||||
|
||||
receive_imf(&alice, mime, false).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
|
||||
assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unknown_sender() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
tcm.execute_securejoin(&alice, &bob).await;
|
||||
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob])
|
||||
.await;
|
||||
|
||||
let sent = alice.send_text(alice_chat_id, "Hi!").await;
|
||||
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
|
||||
|
||||
let sent = bob.send_text(bob_chat_id, "Hi hi!").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;
|
||||
|
||||
// The message from Bob is delivered late, Bob is already removed.
|
||||
let msg = alice.recv_msg(&sent).await;
|
||||
assert_eq!(msg.text, "Hi hi!");
|
||||
assert_eq!(msg.error.unwrap(), "Unknown sender for this chat.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that Bob gets Alice as verified
|
||||
/// if `vc-contact-confirm` is lost but Alice then sends
|
||||
/// a message to Bob in a verified 1:1 chat with a `Chat-Verified` header.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lost_contact_confirm() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
for t in [&alice, &bob] {
|
||||
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
|
||||
// vc-request
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
// vc-auth-required
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
|
||||
// vc-request-with-auth
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
// Alice has Bob verified now.
|
||||
let contact_bob_id =
|
||||
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
||||
|
||||
// Alice sends vc-contact-confirm, but it gets lost.
|
||||
let _sent_vc_contact_confirm = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap();
|
||||
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), false);
|
||||
|
||||
// Alice sends a text message to Bob.
|
||||
let received_hello = tcm.send_recv(&alice, &bob, "Hello!").await;
|
||||
let chat_id = received_hello.chat_id;
|
||||
let chat = Chat::load_from_db(&bob, chat_id).await.unwrap();
|
||||
assert_eq!(chat.is_protected(), true);
|
||||
|
||||
// Received text message in a verified 1:1 chat results in backward verification
|
||||
// and Bob now marks alice as verified.
|
||||
let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap();
|
||||
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true);
|
||||
}
|
||||
|
||||
/// An unencrypted message with already known Autocrypt key, but sent from another address,
|
||||
/// means that it's rather a new contact sharing the same key than the existing one changed its
|
||||
/// address, otherwise it would already have our key to encrypt.
|
||||
///
|
||||
/// This is a regression test for a bug where DC wrongly executed AEAP in this case.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_shared_bobs_key() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
|
||||
|
||||
tcm.execute_securejoin(bob, alice).await;
|
||||
|
||||
let export_dir = tempfile::tempdir().unwrap();
|
||||
imex(bob, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
|
||||
let bob2 = &TestContext::new().await;
|
||||
let bob2_addr = "bob2@example.net";
|
||||
bob2.configure_addr(bob2_addr).await;
|
||||
imex(bob2, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||
|
||||
tcm.execute_securejoin(bob2, alice).await;
|
||||
|
||||
let bob3 = &TestContext::new().await;
|
||||
let bob3_addr = "bob3@example.net";
|
||||
bob3.configure_addr(bob3_addr).await;
|
||||
imex(bob3, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||
tcm.send_recv(bob3, alice, "hi Alice!").await;
|
||||
let msg = tcm.send_recv(alice, bob3, "hi Bob3!").await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let mut bob_ids = HashSet::new();
|
||||
bob_ids.insert(
|
||||
Contact::lookup_id_by_addr(alice, bob_addr, Origin::Unknown)
|
||||
.await?
|
||||
.unwrap(),
|
||||
);
|
||||
bob_ids.insert(
|
||||
Contact::lookup_id_by_addr(alice, bob2_addr, Origin::Unknown)
|
||||
.await?
|
||||
.unwrap(),
|
||||
);
|
||||
bob_ids.insert(
|
||||
Contact::lookup_id_by_addr(alice, bob3_addr, Origin::Unknown)
|
||||
.await?
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(bob_ids.len(), 3);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,841 +0,0 @@
|
||||
use deltachat_contact_tools::{ContactAddress, EmailAddress};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{remove_contact_from_chat, CantSendReason};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::{self, Chattype};
|
||||
use crate::imex::{imex, ImexMode};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, chat_protection_enabled};
|
||||
use crate::test_utils::{get_chat_msg, TimeShiftFalsePositiveNote};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum SetupContactCase {
|
||||
Normal,
|
||||
CheckProtectionTimestamp,
|
||||
WrongAliceGossip,
|
||||
SecurejoinWaitTimeout,
|
||||
AliceIsBot,
|
||||
AliceHasName,
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact() {
|
||||
test_setup_contact_ex(SetupContactCase::Normal).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_protection_timestamp() {
|
||||
test_setup_contact_ex(SetupContactCase::CheckProtectionTimestamp).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_wrong_alice_gossip() {
|
||||
test_setup_contact_ex(SetupContactCase::WrongAliceGossip).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_wait_timeout() {
|
||||
test_setup_contact_ex(SetupContactCase::SecurejoinWaitTimeout).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_alice_is_bot() {
|
||||
test_setup_contact_ex(SetupContactCase::AliceIsBot).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_alice_has_name() {
|
||||
test_setup_contact_ex(SetupContactCase::AliceHasName).await
|
||||
}
|
||||
|
||||
async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
let _n = TimeShiftFalsePositiveNote;
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
|
||||
if case == SetupContactCase::AliceHasName {
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let bob = tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
||||
.await
|
||||
.unwrap();
|
||||
let alice_auto_submitted_hdr;
|
||||
match case {
|
||||
SetupContactCase::AliceIsBot => {
|
||||
alice.set_config_bool(Config::Bot, true).await.unwrap();
|
||||
alice_auto_submitted_hdr = "Auto-Submitted: auto-generated";
|
||||
}
|
||||
_ => alice_auto_submitted_hdr = "Auto-Submitted: auto-replied",
|
||||
};
|
||||
for t in [&alice, &bob] {
|
||||
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
0
|
||||
);
|
||||
|
||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||
// We want Bob to learn Alice's name from their messages, not from the QR code.
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice Exampleorg"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 2: Bob scans QR-code, sends vc-request
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
|
||||
// Step 3: Alice receives vc-request, sends vc-auth-required
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
||||
assert!(!sent.payload.contains("Alice Exampleorg"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-auth-required"
|
||||
);
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false);
|
||||
assert_eq!(
|
||||
bob_chat.why_cant_send(&bob).await.unwrap(),
|
||||
Some(CantSendReason::SecurejoinWait)
|
||||
);
|
||||
if case == SetupContactCase::SecurejoinWaitTimeout {
|
||||
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT));
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
}
|
||||
|
||||
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Check Bob sent the right message.
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
let mut msg = alice.parse_msg(&sent).await;
|
||||
let vc_request_with_auth_ts_sent = msg
|
||||
.get_header(HeaderDef::Date)
|
||||
.and_then(|value| mailparse::dateparse(value).ok())
|
||||
.unwrap();
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.dc_fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
);
|
||||
|
||||
if case == SetupContactCase::WrongAliceGossip {
|
||||
let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
|
||||
let alice_pubkey = msg
|
||||
.gossiped_keys
|
||||
.insert(alice_addr.to_string(), wrong_pubkey)
|
||||
.unwrap();
|
||||
let contact_bob = alice.add_or_lookup_contact(&bob).await;
|
||||
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
|
||||
|
||||
msg.gossiped_keys
|
||||
.insert(alice_addr.to_string(), alice_pubkey)
|
||||
.unwrap();
|
||||
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||
assert!(contact_bob.is_verified(&alice.ctx).await.unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
|
||||
assert_eq!(contact_bob.get_authname(), "");
|
||||
|
||||
if case == SetupContactCase::CheckProtectionTimestamp {
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
}
|
||||
|
||||
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
|
||||
assert!(contact_bob.get_name().is_empty());
|
||||
assert_eq!(contact_bob.is_bot(), false);
|
||||
|
||||
// exactly one one-to-one chat should be visible for both now
|
||||
// (check this before calling alice.create_chat() explicitly below)
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
|
||||
// Check Alice got the verified message in her 1:1 chat.
|
||||
{
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
if case == SetupContactCase::CheckProtectionTimestamp {
|
||||
assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure Alice hasn't yet sent their name to Bob.
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
match case {
|
||||
SetupContactCase::AliceHasName => assert_eq!(contact_alice.get_authname(), "Alice"),
|
||||
_ => assert_eq!(contact_alice.get_authname(), ""),
|
||||
};
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
||||
assert!(!sent.payload.contains("Alice Exampleorg"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-contact-confirm"
|
||||
);
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), false);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_alice.get_authname(), "Alice Exampleorg");
|
||||
assert!(contact_alice.get_name().is_empty());
|
||||
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
|
||||
|
||||
if case != SetupContactCase::SecurejoinWaitTimeout {
|
||||
// Later we check that the timeout message isn't added to the already protected chat.
|
||||
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT + 1));
|
||||
assert_eq!(
|
||||
bob_chat
|
||||
.check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||
.await
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// Check Bob got expected info messages in his 1:1 chat.
|
||||
let msg_cnt: usize = match case {
|
||||
SetupContactCase::SecurejoinWaitTimeout => 3,
|
||||
_ => 2,
|
||||
};
|
||||
let mut i = 0..msg_cnt;
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
||||
if case == SetupContactCase::SecurejoinWaitTimeout {
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(
|
||||
msg.get_text(),
|
||||
stock_str::securejoin_wait_timeout(&bob).await
|
||||
);
|
||||
}
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_bad_qr() {
|
||||
let bob = TestContext::new_bob().await;
|
||||
let ret = join_securejoin(&bob.ctx, "not a qr code").await;
|
||||
assert!(ret.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Ensure Bob knows Alice_FP
|
||||
let alice_pubkey = load_self_public_key(&alice.ctx).await?;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.org".into(),
|
||||
last_seen: 10,
|
||||
last_seen_autocrypt: 10,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
public_key: Some(alice_pubkey.clone()),
|
||||
public_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
||||
gossip_key: Some(alice_pubkey.clone()),
|
||||
gossip_timestamp: 10,
|
||||
gossip_key_fingerprint: Some(alice_pubkey.dc_fingerprint()),
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
backward_verified_key_id: None,
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
peerstate.save_to_db(&bob.ctx.sql).await?;
|
||||
|
||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await?;
|
||||
|
||||
// Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Check Bob sent the right handshake message.
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
||||
&alice.ctx,
|
||||
"",
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
.await?;
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
||||
|
||||
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-contact-confirm"
|
||||
);
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
||||
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_concurrent_calls() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// do a scan that is not working as claire is never responding
|
||||
let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012";
|
||||
let claire_id = join_securejoin(&bob, qr_stale).await?;
|
||||
let chat = Chat::load_from_db(&bob, claire_id).await?;
|
||||
assert!(!claire_id.is_special());
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert!(bob.pop_sent_msg().await.payload().contains("claire@foo.de"));
|
||||
|
||||
// subsequent scans shall abort existing ones or run concurrently -
|
||||
// but they must not fail as otherwise the whole qr scanning becomes unusable until restart.
|
||||
let qr = get_securejoin_qr(&alice, None).await?;
|
||||
let alice_id = join_securejoin(&bob, &qr).await?;
|
||||
let chat = Chat::load_from_db(&bob, alice_id).await?;
|
||||
assert!(!alice_id.is_special());
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert_ne!(claire_id, alice_id);
|
||||
assert!(bob
|
||||
.pop_sent_msg()
|
||||
.await
|
||||
.payload()
|
||||
.contains("alice@example.org"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_secure_join() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// We start with empty chatlists.
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
|
||||
|
||||
let alice_chatid =
|
||||
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
|
||||
|
||||
// Step 1: Generate QR-code, secure-join implied by chatid
|
||||
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 2: Bob scans QR-code, sends vg-request
|
||||
let bob_chatid = join_securejoin(&bob.ctx, &qr).await?;
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert_eq!(
|
||||
sent.recipient(),
|
||||
EmailAddress::new("alice@example.org").unwrap()
|
||||
);
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
|
||||
// Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`,
|
||||
// but it was only used by Alice in `vg-request-with-auth`.
|
||||
// New Delta Chat versions do not use `Secure-Join-Group` header at all
|
||||
// and it is deprecated.
|
||||
// Now `Secure-Join-Group` header
|
||||
// is only sent in `vg-request-with-auth` for compatibility.
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinGroup).is_none());
|
||||
|
||||
// Step 3: Alice receives vg-request, sends vg-auth-required
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vg-auth-required"
|
||||
);
|
||||
|
||||
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::SecurejoinJoinerProgress { .. }))
|
||||
.await;
|
||||
match event {
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => {
|
||||
let alice_contact_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
assert_eq!(contact_id, alice_contact_id);
|
||||
assert_eq!(progress, 400);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Check Bob sent the right handshake message.
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vg-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.dc_fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||
.await?
|
||||
.expect("Contact not found");
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
|
||||
|
||||
// Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vg-member-added"
|
||||
);
|
||||
// Formally this message is auto-submitted, but as the member addition is a result of an
|
||||
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
|
||||
// be strange to have it in "member-added" messages of verified groups only.
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
// This is a two-member group, but Alice must Autocrypt-gossip to her other devices.
|
||||
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
|
||||
|
||||
{
|
||||
// Now Alice's chat with Bob should still be hidden, the verified message should
|
||||
// appear in the group chat.
|
||||
|
||||
let chat = alice.get_chat(&bob).await;
|
||||
assert_eq!(
|
||||
chat.blocked,
|
||||
Blocked::Yes,
|
||||
"Alice's 1:1 chat with Bob is not hidden"
|
||||
);
|
||||
// There should be 3 messages in the chat:
|
||||
// - The ChatProtectionEnabled message
|
||||
// - You added member bob@example.net
|
||||
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
}
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id =
|
||||
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
||||
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
|
||||
|
||||
// Step 7: Bob receives vg-member-added
|
||||
bob.recv_msg(&sent).await;
|
||||
{
|
||||
// Bob has Alice verified, message shows up in the group chat.
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
|
||||
let chat = bob.get_chat(&alice).await;
|
||||
assert_eq!(
|
||||
chat.blocked,
|
||||
Blocked::Yes,
|
||||
"Bob's 1:1 chat with Alice is not hidden"
|
||||
);
|
||||
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid).await.unwrap() {
|
||||
if let chat::ChatItem::Message { msg_id } = item {
|
||||
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
|
||||
let text = msg.get_text();
|
||||
println!("msg {msg_id} text: {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
|
||||
assert!(bob_chat.is_protected());
|
||||
assert!(bob_chat.typ == Chattype::Group);
|
||||
|
||||
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
|
||||
// The one-to-one chats are used internally for the hidden handshake messages,
|
||||
// however, should not be visible in the UIs.
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
|
||||
|
||||
// If Bob then sends a direct message to alice, however, the one-to-one with Alice should appear.
|
||||
let bobs_chat_with_alice = bob.create_chat(&alice).await;
|
||||
let sent = bob.send_text(bobs_chat_with_alice.id, "Hello").await;
|
||||
alice.recv_msg(&sent).await;
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 2);
|
||||
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_group_no_qr() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let mime = br#"Subject: First thread
|
||||
Message-ID: first@example.org
|
||||
To: Alice <alice@example.org>, Bob <bob@example.net>
|
||||
From: Claire <claire@example.org>
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
First thread."#;
|
||||
|
||||
receive_imf(&alice, mime, false).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
|
||||
assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unknown_sender() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
tcm.execute_securejoin(&alice, &bob).await;
|
||||
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob])
|
||||
.await;
|
||||
|
||||
let sent = alice.send_text(alice_chat_id, "Hi!").await;
|
||||
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
|
||||
|
||||
let sent = bob.send_text(bob_chat_id, "Hi hi!").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;
|
||||
|
||||
// The message from Bob is delivered late, Bob is already removed.
|
||||
let msg = alice.recv_msg(&sent).await;
|
||||
assert_eq!(msg.text, "Hi hi!");
|
||||
assert_eq!(msg.error.unwrap(), "Unknown sender for this chat.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that Bob gets Alice as verified
|
||||
/// if `vc-contact-confirm` is lost but Alice then sends
|
||||
/// a message to Bob in a verified 1:1 chat with a `Chat-Verified` header.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lost_contact_confirm() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
for t in [&alice, &bob] {
|
||||
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
|
||||
// vc-request
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
// vc-auth-required
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
|
||||
// vc-request-with-auth
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
// Alice has Bob verified now.
|
||||
let contact_bob_id = Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
||||
|
||||
// Alice sends vc-contact-confirm, but it gets lost.
|
||||
let _sent_vc_contact_confirm = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob, "alice@example.org", Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap();
|
||||
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), false);
|
||||
|
||||
// Alice sends a text message to Bob.
|
||||
let received_hello = tcm.send_recv(&alice, &bob, "Hello!").await;
|
||||
let chat_id = received_hello.chat_id;
|
||||
let chat = Chat::load_from_db(&bob, chat_id).await.unwrap();
|
||||
assert_eq!(chat.is_protected(), true);
|
||||
|
||||
// Received text message in a verified 1:1 chat results in backward verification
|
||||
// and Bob now marks alice as verified.
|
||||
let contact_alice = Contact::get_by_id(&bob, contact_alice_id).await.unwrap();
|
||||
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true);
|
||||
}
|
||||
|
||||
/// An unencrypted message with already known Autocrypt key, but sent from another address,
|
||||
/// means that it's rather a new contact sharing the same key than the existing one changed its
|
||||
/// address, otherwise it would already have our key to encrypt.
|
||||
///
|
||||
/// This is a regression test for a bug where DC wrongly executed AEAP in this case.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_shared_bobs_key() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
|
||||
|
||||
tcm.execute_securejoin(bob, alice).await;
|
||||
|
||||
let export_dir = tempfile::tempdir().unwrap();
|
||||
imex(bob, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
|
||||
let bob2 = &TestContext::new().await;
|
||||
let bob2_addr = "bob2@example.net";
|
||||
bob2.configure_addr(bob2_addr).await;
|
||||
imex(bob2, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||
|
||||
tcm.execute_securejoin(bob2, alice).await;
|
||||
|
||||
let bob3 = &TestContext::new().await;
|
||||
let bob3_addr = "bob3@example.net";
|
||||
bob3.configure_addr(bob3_addr).await;
|
||||
imex(bob3, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||
tcm.send_recv(bob3, alice, "hi Alice!").await;
|
||||
let msg = tcm.send_recv(alice, bob3, "hi Bob3!").await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let mut bob_ids = HashSet::new();
|
||||
bob_ids.insert(
|
||||
Contact::lookup_id_by_addr(alice, bob_addr, Origin::Unknown)
|
||||
.await?
|
||||
.unwrap(),
|
||||
);
|
||||
bob_ids.insert(
|
||||
Contact::lookup_id_by_addr(alice, bob2_addr, Origin::Unknown)
|
||||
.await?
|
||||
.unwrap(),
|
||||
);
|
||||
bob_ids.insert(
|
||||
Contact::lookup_id_by_addr(alice, bob3_addr, Origin::Unknown)
|
||||
.await?
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(bob_ids.len(), 3);
|
||||
Ok(())
|
||||
}
|
||||
@@ -103,7 +103,7 @@ impl Smtp {
|
||||
}
|
||||
|
||||
/// Connect using the provided login params.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn connect(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
|
||||
@@ -45,7 +45,7 @@ async fn new_smtp_transport<S: AsyncBufRead + AsyncWrite + Unpin>(
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn connect_and_auth(
|
||||
context: &Context,
|
||||
proxy_config: &Option<ProxyConfig>,
|
||||
|
||||
426
src/sql.rs
426
src/sql.rs
@@ -44,6 +44,12 @@ macro_rules! params_slice {
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn params_iter(
|
||||
iter: &[impl crate::sql::ToSql],
|
||||
) -> impl Iterator<Item = &dyn crate::sql::ToSql> {
|
||||
iter.iter().map(|item| item as &dyn crate::sql::ToSql)
|
||||
}
|
||||
|
||||
mod migrations;
|
||||
mod pool;
|
||||
|
||||
@@ -254,13 +260,9 @@ impl Sql {
|
||||
let mut blob = BlobObject::new_from_path(context, avatar.as_ref()).await?;
|
||||
match blob.recode_to_avatar_size(context).await {
|
||||
Ok(()) => {
|
||||
if let Some(path) = blob.to_abs_path().to_str() {
|
||||
context
|
||||
.set_config_internal(Config::Selfavatar, Some(path))
|
||||
.await?;
|
||||
} else {
|
||||
warn!(context, "Setting selfavatar failed: non-UTF-8 filename");
|
||||
}
|
||||
context
|
||||
.set_config_internal(Config::Selfavatar, Some(&avatar))
|
||||
.await?
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
|
||||
@@ -439,7 +441,7 @@ impl Sql {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Execute the function inside a transaction assuming that it does writes.
|
||||
/// Execute the function inside a transaction assuming that it does write queries.
|
||||
///
|
||||
/// If the function returns an error, the transaction will be rolled back. If it does not return an
|
||||
/// error, the transaction will be committed.
|
||||
@@ -448,28 +450,7 @@ impl Sql {
|
||||
H: Send + 'static,
|
||||
G: Send + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
|
||||
{
|
||||
let query_only = false;
|
||||
self.transaction_ex(query_only, callback).await
|
||||
}
|
||||
|
||||
/// Execute the function inside a transaction.
|
||||
///
|
||||
/// * `query_only` - Whether the function only executes read statements (queries) and can be run
|
||||
/// in parallel with other transactions. NB: Creating and modifying temporary tables are also
|
||||
/// allowed with `query_only`, temporary tables aren't visible in other connections, but you
|
||||
/// need to pass `PRAGMA query_only=0;` to SQLite before that:
|
||||
/// `pragma_update(None, "query_only", "0")`.
|
||||
/// Also temporary tables need to be dropped because the connection is returned to the pool
|
||||
/// then.
|
||||
///
|
||||
/// If the function returns an error, the transaction will be rolled back. If it does not return
|
||||
/// an error, the transaction will be committed.
|
||||
pub async fn transaction_ex<G, H>(&self, query_only: bool, callback: G) -> Result<H>
|
||||
where
|
||||
H: Send + 'static,
|
||||
G: Send + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
|
||||
{
|
||||
self.call(query_only, move |conn| {
|
||||
self.call_write(move |conn| {
|
||||
let mut transaction = conn.transaction()?;
|
||||
let ret = callback(&mut transaction);
|
||||
|
||||
@@ -1043,5 +1024,388 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to return comma-separated sequence of `?` chars.
|
||||
///
|
||||
/// Use this together with [`rusqlite::ParamsFromIter`] to use dynamically generated
|
||||
/// parameter lists.
|
||||
pub fn repeat_vars(count: usize) -> String {
|
||||
let mut s = "?,".repeat(count);
|
||||
s.pop(); // Remove trailing comma
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod sql_tests;
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{test_utils::TestContext, EventType};
|
||||
|
||||
#[test]
|
||||
fn test_maybe_add_file() {
|
||||
let mut files = Default::default();
|
||||
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
||||
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
||||
maybe_add_file(&mut files, "world2.txt");
|
||||
maybe_add_file(&mut files, "$BLOBDIR");
|
||||
|
||||
assert!(files.contains("hello"));
|
||||
assert!(files.contains("world.txt"));
|
||||
assert!(!files.contains("world2.txt"));
|
||||
assert!(!files.contains("$BLOBDIR"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_file_in_use() {
|
||||
let mut files = Default::default();
|
||||
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
||||
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
||||
maybe_add_file(&mut files, "world2.txt");
|
||||
|
||||
assert!(is_file_in_use(&files, None, "hello"));
|
||||
assert!(!is_file_in_use(&files, Some(".txt"), "hello"));
|
||||
assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_table_exists() {
|
||||
let t = TestContext::new().await;
|
||||
assert!(t.ctx.sql.table_exists("msgs").await.unwrap());
|
||||
assert!(!t.ctx.sql.table_exists("foobar").await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_col_exists() {
|
||||
let t = TestContext::new().await;
|
||||
assert!(t.ctx.sql.col_exists("msgs", "mime_modified").await.unwrap());
|
||||
assert!(!t.ctx.sql.col_exists("msgs", "foobar").await.unwrap());
|
||||
assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap());
|
||||
}
|
||||
|
||||
/// Tests that auto_vacuum is enabled for new databases.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_auto_vacuum() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let query_only = true;
|
||||
let auto_vacuum = t
|
||||
.sql
|
||||
.call(query_only, |conn| {
|
||||
let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| {
|
||||
let auto_vacuum: i32 = row.get(0)?;
|
||||
Ok(auto_vacuum)
|
||||
})?;
|
||||
Ok(auto_vacuum)
|
||||
})
|
||||
.await?;
|
||||
|
||||
// auto_vacuum=2 is the same as auto_vacuum=INCREMENTAL
|
||||
assert_eq!(auto_vacuum, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_housekeeping_db_closed() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let event_source = t.get_event_emitter();
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
|
||||
|
||||
t.sql.close().await;
|
||||
housekeeping(&t).await.unwrap(); // housekeeping should emit warnings but not fail
|
||||
t.sql.open(&t, "".to_string()).await.unwrap();
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
|
||||
|
||||
while let Ok(event) = event_source.try_recv() {
|
||||
match event.typ {
|
||||
EventType::Info(s) => assert!(
|
||||
!s.contains("Keeping new unreferenced file"),
|
||||
"File {s} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)"
|
||||
),
|
||||
EventType::Error(s) => panic!("{}", s),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test for a bug where housekeeping deleted drafts since their
|
||||
/// `hidden` flag is set.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_housekeeping_dont_delete_drafts() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
|
||||
let mut new_draft = Message::new_text("This is my draft".to_string());
|
||||
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
|
||||
|
||||
housekeeping(&t).await.unwrap();
|
||||
|
||||
let loaded_draft = chat.id.get_draft(&t).await.unwrap();
|
||||
assert_eq!(loaded_draft.unwrap().text, "This is my draft");
|
||||
}
|
||||
|
||||
/// Tests that `housekeeping` deletes the blobs backup dir which is created normally by
|
||||
/// `imex::import_backup`.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_housekeeping_delete_blobs_backup_dir() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let dir = t.get_blobdir().join(BLOBS_BACKUP_NAME);
|
||||
tokio::fs::create_dir(&dir).await.unwrap();
|
||||
tokio::fs::write(dir.join("f"), "").await.unwrap();
|
||||
housekeeping(&t).await.unwrap();
|
||||
tokio::fs::create_dir(&dir).await.unwrap();
|
||||
}
|
||||
|
||||
/// Regression test.
|
||||
///
|
||||
/// Previously the code checking for existence of `config` table
|
||||
/// checked it with `PRAGMA table_info("config")` but did not
|
||||
/// drain `SqlitePool.fetch` result, only using the first row
|
||||
/// returned. As a result, prepared statement for `PRAGMA` was not
|
||||
/// finalized early enough, leaving reader connection in a broken
|
||||
/// state after reopening the database, when `config` table
|
||||
/// existed and `PRAGMA` returned non-empty result.
|
||||
///
|
||||
/// Statements were not finalized due to a bug in sqlx:
|
||||
/// <https://github.com/launchbadge/sqlx/issues/1147>
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_db_reopen() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Create database with all the tables.
|
||||
sql.open(&t, "".to_string()).await.unwrap();
|
||||
sql.close().await;
|
||||
|
||||
// Reopen the database
|
||||
sql.open(&t, "".to_string()).await?;
|
||||
sql.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||
("foo", "bar"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let value: Option<String> = sql
|
||||
.query_get_value("SELECT value FROM config WHERE keyname=?;", ("foo",))
|
||||
.await?;
|
||||
assert_eq!(value.unwrap(), "bar");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_migration_flags() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
t.evtracker.get_info_contains("Opened database").await;
|
||||
|
||||
// as migrations::run() was already executed on context creation,
|
||||
// another call should not result in any action needed.
|
||||
// this test catches some bugs where dbversion was forgotten to be persisted.
|
||||
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
|
||||
migrations::run(&t, &t.sql).await?;
|
||||
assert!(!recalc_fingerprints);
|
||||
assert!(!update_icons);
|
||||
assert!(!disable_server_delete);
|
||||
assert!(!recode_avatar);
|
||||
|
||||
info!(&t, "test_migration_flags: XXX END MARKER");
|
||||
|
||||
loop {
|
||||
let evt = t
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::Info(_)))
|
||||
.await;
|
||||
match evt {
|
||||
EventType::Info(msg) => {
|
||||
assert!(
|
||||
!msg.contains("[migration]"),
|
||||
"Migrations were run twice, you probably forgot to update the db version"
|
||||
);
|
||||
if msg.contains("test_migration_flags: XXX END MARKER") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
|
||||
sql.check_passphrase("foo".to_string()).await?;
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database first time")?;
|
||||
sql.close().await;
|
||||
|
||||
// Reopen the database
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Test that we can't open encrypted database without a passphrase.
|
||||
assert!(sql.open(&t, "".to_string()).await.is_err());
|
||||
|
||||
// Now open the database with passpharse, it should succeed.
|
||||
sql.check_passphrase("foo".to_string()).await?;
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sql_change_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database first time")?;
|
||||
sql.close().await;
|
||||
|
||||
// Change the passphrase from "foo" to "bar".
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
sql.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("failed to change passphrase")?;
|
||||
|
||||
// Test that at least two connections are still working.
|
||||
// This ensures that not only the connection which changed the password is working,
|
||||
// but other connections as well.
|
||||
{
|
||||
let lock = sql.pool.read().await;
|
||||
let pool = lock.as_ref().unwrap();
|
||||
let query_only = true;
|
||||
let conn1 = pool.get(query_only).await?;
|
||||
let conn2 = pool.get(query_only).await?;
|
||||
conn1
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
conn2
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
sql.close().await;
|
||||
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Test that old passphrase is not working.
|
||||
assert!(sql.open(&t, "foo".to_string()).await.is_err());
|
||||
|
||||
// Open the database with the new passphrase.
|
||||
sql.check_passphrase("bar".to_string()).await?;
|
||||
sql.open(&t, "bar".to_string())
|
||||
.await
|
||||
.context("failed to open the database third time")?;
|
||||
sql.close().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_query_only() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// `query_row` does not acquire write lock
|
||||
// and operates on read-only connection.
|
||||
// Using it to `INSERT` should fail.
|
||||
let res = t
|
||||
.sql
|
||||
.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 1",
|
||||
("xyz", "ijk"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
|
||||
// If you want to `INSERT` and get value via `RETURNING`,
|
||||
// use `call_write` or `transaction`.
|
||||
|
||||
let res: Result<u32> = t
|
||||
.sql
|
||||
.call_write(|conn| {
|
||||
let val = conn.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 2",
|
||||
("foo", "bar"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)?;
|
||||
Ok(val)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(res.unwrap(), 2);
|
||||
|
||||
let res = t
|
||||
.sql
|
||||
.transaction(|t| {
|
||||
let val = t.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 3",
|
||||
("abc", "def"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)?;
|
||||
Ok(val)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(res.unwrap(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that incremental_vacuum does not fail.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_incremental_vacuum() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
incremental_vacuum(&t).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);"#,
|
||||
r#"
|
||||
ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;
|
||||
CREATE INDEX chats_index2 ON chats (archived);
|
||||
-- 'starred' column is not used currently
|
||||
-- (dropping is not easily doable and stop adding it will make reusing it complicated)
|
||||
ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;
|
||||
CREATE INDEX msgs_index5 ON msgs (starred);"#,
|
||||
17,
|
||||
@@ -1121,7 +1123,11 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
|
||||
inc_and_check(&mut migration_version, 127)?;
|
||||
if dbversion < migration_version {
|
||||
// This is buggy: `delete_server_after` > 1 isn't handled. Migration #129 fixes this.
|
||||
// Existing chatmail configurations having `delete_server_after` disabled should get
|
||||
// `bcc_self` enabled, they may be multidevice configurations because before,
|
||||
// `delete_server_after` was set to 0 upon a backup export for them, but together with this
|
||||
// migration `bcc_self` is enabled instead (whose default is changed to 0 for chatmail). We
|
||||
// don't check `is_chatmail` for simplicity.
|
||||
sql.execute_migration(
|
||||
"INSERT OR IGNORE INTO config (keyname, value)
|
||||
SELECT 'bcc_self', '1'
|
||||
@@ -1150,25 +1156,6 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 129)?;
|
||||
if dbversion < migration_version {
|
||||
// Existing chatmail configurations having `delete_server_after` != "delete at once" should
|
||||
// get `bcc_self` enabled, they may be multidevice configurations:
|
||||
// - Before migration #127, `delete_server_after` was set to 0 upon a backup export, but
|
||||
// then `bcc_self` is enabled instead (whose default is changed to 0 for chatmail).
|
||||
// - The user might set `delete_server_after` to a value other than 0 or 1 when that was
|
||||
// possible in UIs.
|
||||
// We don't check `is_chatmail` for simplicity.
|
||||
sql.execute_migration(
|
||||
"INSERT OR IGNORE INTO config (keyname, value)
|
||||
SELECT 'bcc_self', '1'
|
||||
FROM config WHERE keyname='delete_server_after' AND value!='1'
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
@@ -1229,35 +1216,6 @@ impl Sql {
|
||||
.await
|
||||
.with_context(|| format!("execute_migration failed for version {version}"))?;
|
||||
|
||||
self.config_cache.write().await.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_clear_config_cache() -> anyhow::Result<()> {
|
||||
// Some migrations change the `config` table in SQL.
|
||||
// This test checks that the config cache is invalidated in `execute_migration()`.
|
||||
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(t.get_config_bool(Config::IsChatmail).await?, false);
|
||||
|
||||
t.sql
|
||||
.execute_migration(
|
||||
"INSERT INTO config (keyname, value) VALUES ('is_chatmail', '1')",
|
||||
1000,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(t.get_config_bool(Config::IsChatmail).await?, true);
|
||||
assert_eq!(t.sql.get_raw_config_int(VERSION_CFG).await?.unwrap(), 1000);
|
||||
|
||||
Ok(())
|
||||
self.set_db_version_in_cache(version).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
use super::*;
|
||||
use crate::{test_utils::TestContext, EventType};
|
||||
|
||||
#[test]
|
||||
fn test_maybe_add_file() {
|
||||
let mut files = Default::default();
|
||||
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
||||
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
||||
maybe_add_file(&mut files, "world2.txt");
|
||||
maybe_add_file(&mut files, "$BLOBDIR");
|
||||
|
||||
assert!(files.contains("hello"));
|
||||
assert!(files.contains("world.txt"));
|
||||
assert!(!files.contains("world2.txt"));
|
||||
assert!(!files.contains("$BLOBDIR"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_file_in_use() {
|
||||
let mut files = Default::default();
|
||||
maybe_add_file(&mut files, "$BLOBDIR/hello");
|
||||
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
|
||||
maybe_add_file(&mut files, "world2.txt");
|
||||
|
||||
assert!(is_file_in_use(&files, None, "hello"));
|
||||
assert!(!is_file_in_use(&files, Some(".txt"), "hello"));
|
||||
assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_table_exists() {
|
||||
let t = TestContext::new().await;
|
||||
assert!(t.ctx.sql.table_exists("msgs").await.unwrap());
|
||||
assert!(!t.ctx.sql.table_exists("foobar").await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_col_exists() {
|
||||
let t = TestContext::new().await;
|
||||
assert!(t.ctx.sql.col_exists("msgs", "mime_modified").await.unwrap());
|
||||
assert!(!t.ctx.sql.col_exists("msgs", "foobar").await.unwrap());
|
||||
assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap());
|
||||
}
|
||||
|
||||
/// Tests that auto_vacuum is enabled for new databases.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_auto_vacuum() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let query_only = true;
|
||||
let auto_vacuum = t
|
||||
.sql
|
||||
.call(query_only, |conn| {
|
||||
let auto_vacuum = conn.pragma_query_value(None, "auto_vacuum", |row| {
|
||||
let auto_vacuum: i32 = row.get(0)?;
|
||||
Ok(auto_vacuum)
|
||||
})?;
|
||||
Ok(auto_vacuum)
|
||||
})
|
||||
.await?;
|
||||
|
||||
// auto_vacuum=2 is the same as auto_vacuum=INCREMENTAL
|
||||
assert_eq!(auto_vacuum, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_housekeeping_db_closed() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let avatar_src = t.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let event_source = t.get_event_emitter();
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
|
||||
|
||||
t.sql.close().await;
|
||||
housekeeping(&t).await.unwrap(); // housekeeping should emit warnings but not fail
|
||||
t.sql.open(&t, "".to_string()).await.unwrap();
|
||||
|
||||
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert_eq!(avatar_bytes, &tokio::fs::read(&a).await.unwrap()[..]);
|
||||
|
||||
while let Ok(event) = event_source.try_recv() {
|
||||
match event.typ {
|
||||
EventType::Info(s) => assert!(
|
||||
!s.contains("Keeping new unreferenced file"),
|
||||
"File {s} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)"
|
||||
),
|
||||
EventType::Error(s) => panic!("{}", s),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test for a bug where housekeeping deleted drafts since their
|
||||
/// `hidden` flag is set.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_housekeeping_dont_delete_drafts() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
|
||||
let mut new_draft = Message::new_text("This is my draft".to_string());
|
||||
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
|
||||
|
||||
housekeeping(&t).await.unwrap();
|
||||
|
||||
let loaded_draft = chat.id.get_draft(&t).await.unwrap();
|
||||
assert_eq!(loaded_draft.unwrap().text, "This is my draft");
|
||||
}
|
||||
|
||||
/// Tests that `housekeeping` deletes the blobs backup dir which is created normally by
|
||||
/// `imex::import_backup`.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_housekeeping_delete_blobs_backup_dir() {
|
||||
let t = TestContext::new_alice().await;
|
||||
let dir = t.get_blobdir().join(BLOBS_BACKUP_NAME);
|
||||
tokio::fs::create_dir(&dir).await.unwrap();
|
||||
tokio::fs::write(dir.join("f"), "").await.unwrap();
|
||||
housekeeping(&t).await.unwrap();
|
||||
tokio::fs::create_dir(&dir).await.unwrap();
|
||||
}
|
||||
|
||||
/// Regression test.
|
||||
///
|
||||
/// Previously the code checking for existence of `config` table
|
||||
/// checked it with `PRAGMA table_info("config")` but did not
|
||||
/// drain `SqlitePool.fetch` result, only using the first row
|
||||
/// returned. As a result, prepared statement for `PRAGMA` was not
|
||||
/// finalized early enough, leaving reader connection in a broken
|
||||
/// state after reopening the database, when `config` table
|
||||
/// existed and `PRAGMA` returned non-empty result.
|
||||
///
|
||||
/// Statements were not finalized due to a bug in sqlx:
|
||||
/// <https://github.com/launchbadge/sqlx/issues/1147>
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_db_reopen() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Create database with all the tables.
|
||||
sql.open(&t, "".to_string()).await.unwrap();
|
||||
sql.close().await;
|
||||
|
||||
// Reopen the database
|
||||
sql.open(&t, "".to_string()).await?;
|
||||
sql.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||
("foo", "bar"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let value: Option<String> = sql
|
||||
.query_get_value("SELECT value FROM config WHERE keyname=?;", ("foo",))
|
||||
.await?;
|
||||
assert_eq!(value.unwrap(), "bar");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_migration_flags() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
t.evtracker.get_info_contains("Opened database").await;
|
||||
|
||||
// as migrations::run() was already executed on context creation,
|
||||
// another call should not result in any action needed.
|
||||
// this test catches some bugs where dbversion was forgotten to be persisted.
|
||||
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
|
||||
migrations::run(&t, &t.sql).await?;
|
||||
assert!(!recalc_fingerprints);
|
||||
assert!(!update_icons);
|
||||
assert!(!disable_server_delete);
|
||||
assert!(!recode_avatar);
|
||||
|
||||
info!(&t, "test_migration_flags: XXX END MARKER");
|
||||
|
||||
loop {
|
||||
let evt = t
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::Info(_)))
|
||||
.await;
|
||||
match evt {
|
||||
EventType::Info(msg) => {
|
||||
assert!(
|
||||
!msg.contains("[migration]"),
|
||||
"Migrations were run twice, you probably forgot to update the db version"
|
||||
);
|
||||
if msg.contains("test_migration_flags: XXX END MARKER") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_check_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
|
||||
sql.check_passphrase("foo".to_string()).await?;
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database first time")?;
|
||||
sql.close().await;
|
||||
|
||||
// Reopen the database
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Test that we can't open encrypted database without a passphrase.
|
||||
assert!(sql.open(&t, "".to_string()).await.is_err());
|
||||
|
||||
// Now open the database with passpharse, it should succeed.
|
||||
sql.check_passphrase("foo".to_string()).await?;
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sql_change_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database first time")?;
|
||||
sql.close().await;
|
||||
|
||||
// Change the passphrase from "foo" to "bar".
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
sql.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("failed to change passphrase")?;
|
||||
|
||||
// Test that at least two connections are still working.
|
||||
// This ensures that not only the connection which changed the password is working,
|
||||
// but other connections as well.
|
||||
{
|
||||
let lock = sql.pool.read().await;
|
||||
let pool = lock.as_ref().unwrap();
|
||||
let query_only = true;
|
||||
let conn1 = pool.get(query_only).await?;
|
||||
let conn2 = pool.get(query_only).await?;
|
||||
conn1
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
conn2
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
sql.close().await;
|
||||
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Test that old passphrase is not working.
|
||||
assert!(sql.open(&t, "foo".to_string()).await.is_err());
|
||||
|
||||
// Open the database with the new passphrase.
|
||||
sql.check_passphrase("bar".to_string()).await?;
|
||||
sql.open(&t, "bar".to_string())
|
||||
.await
|
||||
.context("failed to open the database third time")?;
|
||||
sql.close().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_query_only() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// `query_row` does not acquire write lock
|
||||
// and operates on read-only connection.
|
||||
// Using it to `INSERT` should fail.
|
||||
let res = t
|
||||
.sql
|
||||
.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 1",
|
||||
("xyz", "ijk"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
|
||||
// If you want to `INSERT` and get value via `RETURNING`,
|
||||
// use `call_write` or `transaction`.
|
||||
|
||||
let res: Result<u32> = t
|
||||
.sql
|
||||
.call_write(|conn| {
|
||||
let val = conn.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 2",
|
||||
("foo", "bar"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)?;
|
||||
Ok(val)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(res.unwrap(), 2);
|
||||
|
||||
let res = t
|
||||
.sql
|
||||
.transaction(|t| {
|
||||
let val = t.query_row(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?) RETURNING 3",
|
||||
("abc", "def"),
|
||||
|row| {
|
||||
let res: u32 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)?;
|
||||
Ok(val)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(res.unwrap(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that incremental_vacuum does not fail.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_incremental_vacuum() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
incremental_vacuum(&t).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
188
src/stock_str.rs
188
src/stock_str.rs
@@ -1415,10 +1415,9 @@ impl Context {
|
||||
// add welcome-messages. by the label, this is done only once,
|
||||
// if the user has deleted the message or the chat, it is not added again.
|
||||
let image = include_bytes!("../assets/welcome-image.jpg");
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
|
||||
let blob = BlobObject::create(self, "welcome-image.jpg", image).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
msg.param.set(Param::Filename, "welcome-image.jpg");
|
||||
chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
|
||||
|
||||
let mut msg = Message::new_text(welcome_message(self).await);
|
||||
@@ -1439,4 +1438,187 @@ impl Accounts {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod stock_str_tests;
|
||||
mod tests {
|
||||
use num_traits::ToPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::delete_and_reset_all_device_msgs;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_enum_mapping() {
|
||||
assert_eq!(StockMessage::NoMessages.to_usize().unwrap(), 1);
|
||||
assert_eq!(StockMessage::SelfMsg.to_usize().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback() {
|
||||
assert_eq!(StockMessage::NoMessages.fallback(), "No messages.");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_stock_translation() {
|
||||
let t = TestContext::new().await;
|
||||
t.set_stock_translation(StockMessage::NoMessages, "xyz".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(no_messages(&t).await, "xyz")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_stock_translation_wrong_replacements() {
|
||||
let t = TestContext::new().await;
|
||||
assert!(t
|
||||
.ctx
|
||||
.set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string())
|
||||
.await
|
||||
.is_err());
|
||||
assert!(t
|
||||
.ctx
|
||||
.set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string())
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_str() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(no_messages(&t).await, "No messages.");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_string_repl_str() {
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap();
|
||||
// uses %1$s substitution
|
||||
assert_eq!(
|
||||
contact_verified(&t, &contact).await,
|
||||
"Someone (someone@example.org) verified."
|
||||
);
|
||||
// We have no string using %1$d to test...
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_simple() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
msg_location_enabled(&t).await,
|
||||
"Location streaming enabled."
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_add_member_by_me() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
msg_add_member_remote(&t, "alice@example.org").await,
|
||||
"I added member alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
|
||||
"You added member alice@example.org."
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
Contact::create(&t, "Alice", "alice@example.org")
|
||||
.await
|
||||
.expect("failed to create contact");
|
||||
assert_eq!(
|
||||
msg_add_member_remote(&t, "alice@example.org").await,
|
||||
"I added member alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
|
||||
"You added member Alice (alice@example.org)."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_add_member_by_other_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = {
|
||||
Contact::create(&t, "Alice", "alice@example.org")
|
||||
.await
|
||||
.expect("Failed to create contact Alice");
|
||||
Contact::create(&t, "Bob", "bob@example.com")
|
||||
.await
|
||||
.expect("failed to create bob")
|
||||
};
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, "alice@example.org", contact_id,).await,
|
||||
"Member Alice (alice@example.org) added by Bob (bob@example.com)."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quota_exceeding_stock_str() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let str = quota_exceeding(&t, 81).await;
|
||||
assert!(str.contains("81% "));
|
||||
assert!(str.contains("100% "));
|
||||
assert!(!str.contains("%%"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_download_msg_body() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let str = partial_download_msg_body(&t, 1024 * 1024).await;
|
||||
assert_eq!(str, "1 MiB message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_device_chats() {
|
||||
let t = TestContext::new().await;
|
||||
t.update_device_chats().await.ok();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 2);
|
||||
|
||||
let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let (self_talk_id, device_chat_id) = if chat0.is_self_talk() {
|
||||
(chats.get_chat_id(0).unwrap(), chats.get_chat_id(1).unwrap())
|
||||
} else {
|
||||
(chats.get_chat_id(1).unwrap(), chats.get_chat_id(0).unwrap())
|
||||
};
|
||||
|
||||
// delete self-talk first; this adds a message to device-chat about how self-talk can be restored
|
||||
let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len();
|
||||
self_talk_id.delete(&t).await.ok();
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len(),
|
||||
device_chat_msgs_before + 1
|
||||
);
|
||||
|
||||
// delete device chat
|
||||
device_chat_id.delete(&t).await.ok();
|
||||
|
||||
// check, that the chatlist is empty
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// a subsequent call to update_device_chats() must not re-add manually deleted messages or chats
|
||||
t.update_device_chats().await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// Reset all device messages. This normally happens due to account export and import.
|
||||
// Check that update_device_chats() does not add welcome message for imported account.
|
||||
delete_and_reset_all_device_msgs(&t).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
t.update_device_chats().await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
use num_traits::ToPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::delete_and_reset_all_device_msgs;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_enum_mapping() {
|
||||
assert_eq!(StockMessage::NoMessages.to_usize().unwrap(), 1);
|
||||
assert_eq!(StockMessage::SelfMsg.to_usize().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback() {
|
||||
assert_eq!(StockMessage::NoMessages.fallback(), "No messages.");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_stock_translation() {
|
||||
let t = TestContext::new().await;
|
||||
t.set_stock_translation(StockMessage::NoMessages, "xyz".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(no_messages(&t).await, "xyz")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_stock_translation_wrong_replacements() {
|
||||
let t = TestContext::new().await;
|
||||
assert!(t
|
||||
.ctx
|
||||
.set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string())
|
||||
.await
|
||||
.is_err());
|
||||
assert!(t
|
||||
.ctx
|
||||
.set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string())
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_str() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(no_messages(&t).await, "No messages.");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_string_repl_str() {
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org")
|
||||
.await
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap();
|
||||
// uses %1$s substitution
|
||||
assert_eq!(
|
||||
contact_verified(&t, &contact).await,
|
||||
"Someone (someone@example.org) verified."
|
||||
);
|
||||
// We have no string using %1$d to test...
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_simple() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
msg_location_enabled(&t).await,
|
||||
"Location streaming enabled."
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_add_member_by_me() {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
msg_add_member_remote(&t, "alice@example.org").await,
|
||||
"I added member alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
|
||||
"You added member alice@example.org."
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
Contact::create(&t, "Alice", "alice@example.org")
|
||||
.await
|
||||
.expect("failed to create contact");
|
||||
assert_eq!(
|
||||
msg_add_member_remote(&t, "alice@example.org").await,
|
||||
"I added member alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
|
||||
"You added member Alice (alice@example.org)."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stock_system_msg_add_member_by_other_with_displayname() {
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = {
|
||||
Contact::create(&t, "Alice", "alice@example.org")
|
||||
.await
|
||||
.expect("Failed to create contact Alice");
|
||||
Contact::create(&t, "Bob", "bob@example.com")
|
||||
.await
|
||||
.expect("failed to create bob")
|
||||
};
|
||||
assert_eq!(
|
||||
msg_add_member_local(&t, "alice@example.org", contact_id,).await,
|
||||
"Member Alice (alice@example.org) added by Bob (bob@example.com)."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quota_exceeding_stock_str() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let str = quota_exceeding(&t, 81).await;
|
||||
assert!(str.contains("81% "));
|
||||
assert!(str.contains("100% "));
|
||||
assert!(!str.contains("%%"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_download_msg_body() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let str = partial_download_msg_body(&t, 1024 * 1024).await;
|
||||
assert_eq!(str, "1 MiB message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_device_chats() {
|
||||
let t = TestContext::new().await;
|
||||
t.update_device_chats().await.ok();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 2);
|
||||
|
||||
let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let (self_talk_id, device_chat_id) = if chat0.is_self_talk() {
|
||||
(chats.get_chat_id(0).unwrap(), chats.get_chat_id(1).unwrap())
|
||||
} else {
|
||||
(chats.get_chat_id(1).unwrap(), chats.get_chat_id(0).unwrap())
|
||||
};
|
||||
|
||||
// delete self-talk first; this adds a message to device-chat about how self-talk can be restored
|
||||
let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len();
|
||||
self_talk_id.delete(&t).await.ok();
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t, device_chat_id).await.unwrap().len(),
|
||||
device_chat_msgs_before + 1
|
||||
);
|
||||
|
||||
// delete device chat
|
||||
device_chat_id.delete(&t).await.ok();
|
||||
|
||||
// check, that the chatlist is empty
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// a subsequent call to update_device_chats() must not re-add manually deleted messages or chats
|
||||
t.update_device_chats().await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// Reset all device messages. This normally happens due to account export and import.
|
||||
// Check that update_device_chats() does not add welcome message for imported account.
|
||||
delete_and_reset_all_device_msgs(&t).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
t.update_device_chats().await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
}
|
||||
@@ -286,8 +286,6 @@ impl Message {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::ChatId;
|
||||
use crate::param::Param;
|
||||
@@ -307,90 +305,62 @@ mod tests {
|
||||
.unwrap();
|
||||
let some_text = " bla \t\n\tbla\n\t".to_string();
|
||||
|
||||
async fn write_file_to_blobdir(d: &TestContext) -> PathBuf {
|
||||
let bytes = &[38, 209, 39, 29]; // Just some random bytes
|
||||
let file = d.get_blobdir().join("random_filename_392438");
|
||||
tokio::fs::write(&file, bytes).await.unwrap();
|
||||
file
|
||||
}
|
||||
|
||||
let msg = Message::new_text(some_text.to_string());
|
||||
assert_summary_texts(&msg, ctx, "bla bla").await; // for simple text, the type is not added to the summary
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.jpg", None);
|
||||
assert_summary_texts(&msg, ctx, "📷 Image").await; // file names are not added for images
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_text(some_text.to_string());
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.jpg", None);
|
||||
assert_summary_texts(&msg, ctx, "📷 bla bla").await; // type is visible by emoji if text is set
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::Video);
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.mp4"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.mp4", None);
|
||||
assert_summary_texts(&msg, ctx, "🎥 Video").await; // file names are not added for videos
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::Video);
|
||||
msg.set_text(some_text.to_string());
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.mp4"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.mp4", None);
|
||||
assert_summary_texts(&msg, ctx, "🎥 bla bla").await; // type is visible by emoji if text is set
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::Gif);
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.gif"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.gif", None);
|
||||
assert_summary_texts(&msg, ctx, "GIF").await; // file names are not added for GIFs
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::Gif);
|
||||
msg.set_text(some_text.to_string());
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.gif"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.gif", None);
|
||||
assert_summary_texts(&msg, ctx, "GIF \u{2013} bla bla").await; // file names are not added for GIFs
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.png"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.png", None);
|
||||
assert_summary_texts(&msg, ctx, "Sticker").await; // file names are not added for stickers
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::Voice);
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.mp3", None);
|
||||
assert_summary_texts(&msg, ctx, "🎤 Voice message").await; // file names are not added for voice messages
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::Voice);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.mp3", None);
|
||||
assert_summary_texts(&msg, ctx, "🎤 bla bla").await;
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::Audio);
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.mp3", None);
|
||||
assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; // file name is added for audio
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::Audio);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.mp3", None);
|
||||
assert_summary_texts(&msg, ctx, "🎵 foo.mp3 \u{2013} bla bla").await; // file name and text added for audio
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
let bytes = include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc");
|
||||
msg.set_file_from_bytes(ctx, "foo.xdc", bytes, None)
|
||||
.await
|
||||
.unwrap();
|
||||
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
|
||||
assert_eq!(msg.viewtype, Viewtype::Webxdc);
|
||||
@@ -399,28 +369,24 @@ mod tests {
|
||||
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
|
||||
assert_summary_texts(&msg, ctx, "nice app! \u{2013} bla bla").await;
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_summary_texts(&msg, ctx, "📎 foo.bar").await; // file name is added for files
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_summary_texts(&msg, ctx, "📎 foo.bar \u{2013} bla bla").await; // file name is added for files
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::VideochatInvitation);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.bar", None);
|
||||
assert_summary_texts(&msg, ctx, "Video chat invitation").await; // text is not added for videochat invitations
|
||||
|
||||
let mut msg = Message::new(Viewtype::Vcard);
|
||||
msg.set_file_from_bytes(ctx, "foo.vcf", b"", None).unwrap();
|
||||
msg.set_file_from_bytes(ctx, "foo.vcf", b"", None)
|
||||
.await
|
||||
.unwrap();
|
||||
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
|
||||
// If a vCard can't be parsed, the message becomes `Viewtype::File`.
|
||||
assert_eq!(msg.viewtype, Viewtype::File);
|
||||
@@ -440,6 +406,7 @@ mod tests {
|
||||
END:VCARD",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
|
||||
assert_eq!(msg.viewtype, Viewtype::Vcard);
|
||||
@@ -452,11 +419,9 @@ mod tests {
|
||||
assert_eq!(msg.get_summary_text(ctx).await, "Forwarded: bla bla"); // for simple text, the type is not added to the summary
|
||||
assert_eq!(msg.get_summary_text_without_prefix(ctx).await, "bla bla"); // skipping prefix used for reactions summaries
|
||||
|
||||
let file = write_file_to_blobdir(&d).await;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_text(some_text.clone());
|
||||
msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
|
||||
.unwrap();
|
||||
msg.set_file("foo.bar", None);
|
||||
msg.param.set_int(Param::Forwarded, 1);
|
||||
assert_eq!(
|
||||
msg.get_summary_text(ctx).await,
|
||||
|
||||
79
src/sync.rs
79
src/sync.rs
@@ -16,7 +16,7 @@ use crate::param::Param;
|
||||
use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
|
||||
use crate::token::Namespace;
|
||||
use crate::tools::time;
|
||||
use crate::{message, stock_str, token};
|
||||
use crate::{stock_str, token};
|
||||
|
||||
/// Whether to send device sync messages. Aimed for usage in the internal API.
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -62,10 +62,6 @@ pub(crate) enum SyncData {
|
||||
key: Config,
|
||||
val: String,
|
||||
},
|
||||
SaveMessage {
|
||||
src: String, // RFC724 id (i.e. "Message-Id" header)
|
||||
dest: String, // RFC724 id (i.e. "Message-Id" header)
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -263,7 +259,6 @@ impl Context {
|
||||
DeleteQrToken(token) => self.delete_qr_token(token).await,
|
||||
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
|
||||
SyncData::Config { key, val } => self.sync_config(key, val).await,
|
||||
SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
|
||||
},
|
||||
SyncDataOrUnknown::Unknown(data) => {
|
||||
warn!(self, "Ignored unknown sync item: {data}.");
|
||||
@@ -273,15 +268,6 @@ impl Context {
|
||||
.log_err(self)
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Since there was a sync message, we know that there is a second device.
|
||||
// Set BccSelf to true if it isn't already.
|
||||
if !items.items.is_empty() && !self.get_config_bool(Config::BccSelf).await.unwrap_or(true) {
|
||||
self.set_config_ex(Sync::Nosync, Config::BccSelf, Some("1"))
|
||||
.await
|
||||
.log_err(self)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_qr_token(&self, token: &QrTokenData) -> Result<()> {
|
||||
@@ -296,13 +282,6 @@ impl Context {
|
||||
token::delete(self, Namespace::Auth, &token.auth).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_message(&self, src_rfc724_mid: &str, dest_rfc724_mid: &String) -> Result<()> {
|
||||
if let Some((src_msg_id, _)) = message::rfc724_mid_exists(self, src_rfc724_mid).await? {
|
||||
chat::save_copy_in_self_talk(self, &src_msg_id, dest_rfc724_mid).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -599,62 +578,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_sync_msg_enables_bccself() -> Result<()> {
|
||||
for (chatmail, sync_message_sent) in
|
||||
[(false, false), (false, true), (true, false), (true, true)]
|
||||
{
|
||||
let alice1 = TestContext::new_alice().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
|
||||
// SyncMsgs defaults to true on real devices, but in tests it defaults to false,
|
||||
// so we need to enable it
|
||||
alice1.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
if chatmail {
|
||||
alice1.set_config_bool(Config::IsChatmail, true).await?;
|
||||
alice2.set_config_bool(Config::IsChatmail, true).await?;
|
||||
} else {
|
||||
alice2.set_config_bool(Config::BccSelf, false).await?;
|
||||
}
|
||||
|
||||
alice1.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
let sent_msg = if sync_message_sent {
|
||||
alice1
|
||||
.add_sync_item(SyncData::AddQrToken(QrTokenData {
|
||||
invitenumber: "in".to_string(),
|
||||
auth: "testtoken".to_string(),
|
||||
grpid: None,
|
||||
}))
|
||||
.await?;
|
||||
alice1.send_sync_msg().await?.unwrap();
|
||||
alice1.pop_sent_sync_msg().await
|
||||
} else {
|
||||
let chat = alice1.get_self_chat().await;
|
||||
alice1.send_text(chat.id, "Hi").await
|
||||
};
|
||||
|
||||
// On chatmail accounts, BccSelf defaults to false.
|
||||
// When receiving a sync message from another device,
|
||||
// there obviously is a multi-device-setup, and BccSelf
|
||||
// should be enabled.
|
||||
assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, false);
|
||||
|
||||
alice2.recv_msg_opt(&sent_msg).await;
|
||||
assert_eq!(
|
||||
alice2.get_config_bool(Config::BccSelf).await?,
|
||||
// BccSelf should be enabled when receiving a sync message,
|
||||
// but not when receiving another outgoing message
|
||||
// because we might have forgotten it and it then it might have been forwarded to us again
|
||||
// (though of course this is very unlikely).
|
||||
sync_message_sent
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_bot_no_sync_msgs() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
18
src/tools.rs
18
src/tools.rs
@@ -26,7 +26,7 @@ use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone};
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
#[cfg(test)]
|
||||
pub use deltachat_time::SystemTimeTools as SystemTime;
|
||||
use futures::TryStreamExt;
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use mailparse::dateparse;
|
||||
use mailparse::headers::Headers;
|
||||
use mailparse::MailHeaderMap;
|
||||
@@ -366,6 +366,22 @@ pub(crate) async fn delete_file(context: &Context, path: impl AsRef<Path>) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_files_in_dir(context: &Context, path: impl AsRef<Path>) -> Result<()> {
|
||||
let read_dir = tokio::fs::read_dir(path)
|
||||
.await
|
||||
.context("could not read dir to delete")?;
|
||||
let mut read_dir = tokio_stream::wrappers::ReadDirStream::new(read_dir);
|
||||
while let Some(entry) = read_dir.next().await {
|
||||
match entry {
|
||||
Ok(file) => {
|
||||
delete_file(context, file.file_name()).await?;
|
||||
}
|
||||
Err(e) => warn!(context, "Could not read file to delete: {}", e),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A guard which will remove the path when dropped.
|
||||
///
|
||||
/// It implements [`Deref`] so it can be used as a `&Path`.
|
||||
|
||||
2258
src/webxdc.rs
2258
src/webxdc.rs
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,3 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::chat::{send_msg, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::contact::ContactId;
|
||||
@@ -15,7 +13,7 @@ impl Context {
|
||||
pub async fn set_webxdc_integration(&self, file: &str) -> Result<()> {
|
||||
let chat_id = ChatId::create_for_contact(self, ContactId::SELF).await?;
|
||||
let mut msg = Message::new(Viewtype::Webxdc);
|
||||
msg.set_file_and_deduplicate(self, Path::new(&file), None, None)?;
|
||||
msg.set_file(file, None);
|
||||
msg.hidden = true;
|
||||
msg.param.set_int(Param::WebxdcIntegration, 1);
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1); // needed to pass `internet_access` requirements
|
||||
@@ -192,7 +190,8 @@ mod tests {
|
||||
"mapstest.xdc",
|
||||
include_bytes!("../../test-data/webxdc/mapstest-integration-unset.xdc"),
|
||||
None,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
t.send_msg(self_chat.id, &mut msg).await;
|
||||
assert_integration(&t, "with some icon").await?; // still the default integration
|
||||
|
||||
@@ -203,7 +202,8 @@ mod tests {
|
||||
"mapstest.xdc",
|
||||
include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc"),
|
||||
None,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
let sent = t.send_msg(self_chat.id, &mut msg).await;
|
||||
let info = msg.get_webxdc_info(&t).await?;
|
||||
assert!(info.summary.contains("Used as map"));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +0,0 @@
|
||||
Group#Chat#11: Group [3 member(s)] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#11: info (Contact#Contact#Info): alice@example.org invited you to join this group.
|
||||
|
||||
Waiting for the device of alice@example.org to reply… [NOTICED][INFO]
|
||||
Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
|
||||
Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#18🔒: (Contact#Contact#10): Member Me (fiona@example.net) added by alice@example.org. [FRESH][INFO]
|
||||
--------------------------------------------------------------------------------
|
||||
47
test-data/message/blockquote-tag.eml
Normal file
47
test-data/message/blockquote-tag.eml
Normal file
@@ -0,0 +1,47 @@
|
||||
Return-Path: <bob@example.org>
|
||||
User-Agent: K-9 Mail for Android
|
||||
In-Reply-To: <hasnihae@gmx.de>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative; boundary="----MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Subject: Re: Test
|
||||
To: Alice <alice@example.org>
|
||||
From: Bob <bob@example.org>
|
||||
Message-ID: <haeisnr@example.org>
|
||||
|
||||
------MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0
|
||||
Content-Type: text/definitelynotplainthiswouldbetooeasy;
|
||||
charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Hi Alice,
|
||||
|
||||
some text.
|
||||
|
||||
Am 21=2E Juni 2020 10:38:44 MESZ schrieb Alice <alice@example=2Eorg>:
|
||||
>Dear Bob,
|
||||
>
|
||||
>let's meet
|
||||
>
|
||||
>Alice
|
||||
|
||||
--=20
|
||||
Diese Nachricht wurde von meinem Android-Ger=C3=A4t mit K-9 Mail gesendet=
|
||||
=2E
|
||||
------MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0
|
||||
Content-Type: text/html;
|
||||
charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html><head></head><body>Hi Alice,<br><br>some text.<br><br>
|
||||
<div class=3D"gmail_quote">Am 21=2E Juni 2020 10:38:44 M=
|
||||
ESZ schrieb Alice <jonathanschmiederer@gmx=2Ede>:<bloc=
|
||||
kquote class=3D"gmail_quote" style=3D"margin: 0pt 0pt 0pt 0=2E8ex; border-l=
|
||||
eft: 1px solid rgb(204, 204, 204); padding-left: 1ex;">
|
||||
<pre class=3D"k9mail">Sehr geehrte/r Frau/Herr Brenner,<br><br>ich habe in=
|
||||
meinen JuFo-Unterlagen den angeh=C3=A4ngten Gutschein gefunden=2E<br>Ist e=
|
||||
s noch m=C3=B6glich, diesen einzul=C3=B6sen?<br><br>Mit freundlichen Gr=C3=
|
||||
=BC=C3=9Fen<br>Alice<br></pre></blockquote></div><br>-- <br>=
|
||||
Diese Nachricht wurde von meinem Android-Ger=C3=A4t mit K-9 Mail gesendet=
|
||||
=2E</body></html>
|
||||
------MLV7YOLJ7ED4UZKNGQYQ63O0RJGHU0--
|
||||
Reference in New Issue
Block a user