Compare commits

..

1 Commits

Author SHA1 Message Date
Septias
5cadc5d850 Generate rfc724_mid when creating Message 2025-03-17 18:04:58 +01:00
121 changed files with 2950 additions and 3930 deletions

View File

@@ -1,35 +0,0 @@
---
name: Bug report
about: Report something that isn't working.
title: ''
assignees: ''
labels: bug
---
<!--
This is the chatmail core's bug report tracker.
For Delta Chat feature requests and support, please go to the forum: https://support.delta.chat
Please fill out as much of this form as you can (leaving out stuff that is not applicable is ok).
-->
- Operating System (Linux/Mac/Windows/iOS/Android):
- Core Version:
- Client Version:
## Expected behavior
*What did you try to achieve?*
## Actual behavior
*What happened instead?*
### Steps to reproduce the problem
1.
2.
### Screenshots
### Logs

View File

@@ -20,22 +20,20 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.86.0
# Minimum Supported Rust Version
MSRV: 1.82.0
jobs:
lint_rust:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.84.1
steps:
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
- name: Install rustfmt and clippy
run: rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt --component clippy
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Run rustfmt
@@ -93,36 +91,25 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: latest
rust: 1.84.1
- os: windows-latest
rust: latest
rust: 1.84.1
- os: macos-latest
rust: latest
rust: 1.84.1
# Minimum Supported Rust Version
# Minimum Supported Rust Version = 1.81.0
- os: ubuntu-latest
rust: minimum
rust: 1.81.0
runs-on: ${{ matrix.os }}
steps:
- run:
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
shell: bash
if: matrix.rust == 'minimum'
- run:
echo "RUSTUP_TOOLCHAIN=$RUST_VERSION" >> $GITHUB_ENV
shell: bash
if: matrix.rust == 'latest'
- uses: actions/checkout@v4
with:
show-progress: false
persist-credentials: false
- name: Install Rust ${{ matrix.rust }}
run: rustup toolchain install --profile minimal $RUSTUP_TOOLCHAIN
shell: bash
- run: rustup override set $RUSTUP_TOOLCHAIN
shell: bash
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
- run: rustup override set ${{ matrix.rust }}
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2

View File

@@ -1,218 +1,5 @@
# Changelog
## [1.159.1] - 2025-04-12
### API-Changes
- deltachat-rpc-client: Add `Account.add_transport()`.
- Add jsonrpc for info_contact_id.
### Build system
- Update crossbeam-channel from 0.5.14 to 0.5.15.
- Increase MSRV to 1.82.0.
### CI
- Don't make ruff format quiet ([#6785](https://github.com/chatmail/core/pull/6785)).
### Documentation
- MimeFactory.member_timestamps has the same order as To: rather than RCPT TO:.
- Two JsonRPC doc improvements ([#6778](https://github.com/chatmail/core/pull/6778)).
### Features / Changes
- Improve error message when the user tries to do AEAP ([#6786](https://github.com/chatmail/core/pull/6786)).
- Pass email and password via env in python-jsonrpc.
- Track gossiping per (chat, fingerprint) pair.
### Fixes
- Add missing ChatDeleted event to python jsonrpc client.
- Never send Autocrypt-Gossip in broadcast lists.
- Restart I/O when mvbox_move setting is changed.
### Tests
- Port test_delete_deltachat_folder to JSON-RPC.
- Autocrypt-Gossip header isn't sent in broadcast messages.
- Encrypt test_subject_in_group().
- Encrypt test_remove_member_bcc.
## [1.159.0] - 2025-04-08
### API-Changes
- deltachat-rpc-client: Add Message.get_info().
- CFFI: Add `dc_make_vcard()` and `dc_import_vcard()`.
- Add legacy Python bindings for `make_vcard` and `import_vcard`.
### CI
- Upgrade Rust from 1.84.1 to 1.86.0 ([#6784](https://github.com/chatmail/core/pull/6784)).
### Features / Changes
- Add name resp. "Me" to contact encryption info ([#6720](https://github.com/chatmail/core/pull/6720)).
- Get contact-id for info messages ([#6714](https://github.com/chatmail/core/pull/6714)).
- No unencrypted chat when securejoin times out ([#6722](https://github.com/chatmail/core/pull/6722)).
- Clear `Param::IsEdited` when forwarding a message.
- Remove email address from 'add second device' qr code ([#6760](https://github.com/chatmail/core/pull/6760)).
- Parse Proton Mail vCards again ([#6771](https://github.com/chatmail/core/pull/6771)).
- Do not consider encrypting to the primary OpenPGP key.
### Fixes
- jsonrpc: Fix deadlock in get_all_accounts().
- Set GroupNameTimestamp on group promotion ([#6729](https://github.com/chatmail/core/pull/6729)).
- Encrypt broadcast lists.
### Miscellaneous Tasks
- Update yerpc to 0.6.3.
- cargo: Update textwrap from 0.16.1 to 0.16.2.
- cargo: Bump uuid from 1.15.1 to 1.16.0.
- cargo: Bump libc from 0.2.170 to 0.2.171.
- cargo: Bump anyhow from 1.0.96 to 1.0.97.
- cargo: Bump bytes from 1.10.0 to 1.10.1.
- cargo: Bump once_cell from 1.20.3 to 1.21.3.
- cargo: Bump thiserror from 2.0.11 to 2.0.12.
- cargo: Bump pin-project from 1.1.9 to 1.1.10.
- cargo: Bump hyper-util from 0.1.10 to 0.1.11.
- cargo: Bump log from 0.4.26 to 0.4.27.
- cargo: Bump tokio-util from 0.7.13 to 0.7.14.
- cargo: Bump syn from 2.0.98 to 2.0.100.
- cargo: Bump serde_json from 1.0.139 to 1.0.140.
- cargo: Bump quote from 1.0.38 to 1.0.40.
- cargo: Bump http-body-util from 0.1.2 to 0.1.3.
- cargo: Bump openssl from 0.10.71 to 0.10.72.
- cargo: Bump quick-xml from 0.37.2 to 0.37.4.
- cargo: Bump blake3 from 1.6.1 to 1.8.0.
- cargo: Bump tokio from 1.43.0 to 1.43.1 ([#6780](https://github.com/chatmail/core/pull/6780)).
- Add issue template.
- Add bug label on bug issue template.
- cargo: Bump tokio from 1.43.0 to 1.44.1.
- cargo: Bump fd-lock from 4.0.2 to 4.0.4.
- Update async-smtp from 0.10.0 to 0.10.1.
- Update async-imap from 0.10.3 to 0.10.4.
- cargo: Bump tempfile from 3.14.0 to 3.19.1.
- cargo: Bump image from 0.25.5 to 0.25.6.
- cargo: Bump serde from 1.0.218 to 1.0.219.
### Other
- Add python and tox to flake.nix devshell ([#6233](https://github.com/chatmail/core/pull/6233))
- Update spec wrt edit/delete, minor rewordings ([#6708](https://github.com/chatmail/core/pull/6708))
- Update 'takes longer' fallback wording.
- Handle classic emails as such only in classic profiles ([#6767](https://github.com/chatmail/core/pull/6767))
- Move ASM strings to core, point to "Add Second Device" ([#6777](https://github.com/chatmail/core/pull/6777))
### Refactor
- Replace `once_cell::sync::Lazy` with `std::sync::LazyLock`.
- Move vCard code to its own file ([#6776](https://github.com/chatmail/core/pull/6776)).
### Tests
- Use encryption in more Rust tests.
- Use encryption in all JSON-RPC online tests.
- Encrypt legacy Python tests.
- Send only encrypted messages in online JS tests.
- Add APIs to create `dom@example.net` and `elena@example.net`.
- Split public keys from secret keys in runtime.
- Remove fetch_existing tests.
- Port test_forward_encrypted_to_unencrypted from legacy Python to Rust.
- Port test_one_account_send_bcc_setting from legacy Python to JSON-RPC.
- Port test_multidevice_sync_seen to JSON-RPC.
- Use QR codes to setup contact with test bots.
- Remove flaky key::tests::test_load_self_existing test ([#6763](https://github.com/chatmail/core/pull/6763)).
- Update blob hash in blob::blob_tests::test_selfavatar_outside_blobdir.
## [1.158.0] - 2025-03-29
### API-Changes
- deltachat-rpc-client: Accept `Account` as `Account.create_contact()` argument.
- Rust: Add `ContactId.set_name()`.
- JSON-RPC: Rename parameter name in `get_webxdc_href` to `info_msg_id` to reduce confusion potential ([#6681](https://github.com/chatmail/core/pull/6681)).
### Features / Changes
- Nicer configuration error ([#6684](https://github.com/chatmail/core/pull/6684)).
- securejoin: Do not create 1:1 chat on Alice's side until `vc-request-with-auth`.
- Understandable error message when accounts.lock can't be locked ([#6695](https://github.com/chatmail/core/pull/6695)).
- Simplify e2ee decision logic, remove majority vote.
- Stop saving txt_raw.
### Fixes
- Do not fail to send the message if some keys are missing.
- Synchronize contact name changes.
- Move group name timestamp update up in create_send_msg_jobs().
- Fixes for transport JSON-RPC ([#6680](https://github.com/chatmail/core/pull/6680)).
### Build system
- deltachat-rpc-client: Move development dependencies from tox.ini to pyproject.toml.
- Update resolve-conf from 0.7.0 to 0.7.1.
### Refactor
- Do not convert SQL arguments to `String` unnecessarily.
- Factor out `update_chat_names()`.
- Use `created_timestamp()` instead of duplicating its code ([#6692](https://github.com/chatmail/core/pull/6692)).
- Use `chat_id.get_timestamp()` instead of duplicating its code ([#6691](https://github.com/chatmail/core/pull/6691)).
- Move `mark_recipients_as_verified()` call out of `has_verified_encryption()`.
- Move `proxy_config` out of `ConfiguredLoginParam` ([#6712](https://github.com/chatmail/core/pull/6712)).
### Tests
- Use vCard in TestContext.add_or_lookup_contact().
- Remove test_group_with_removed_message_id.
- Use add_or_lookup_email_contact() in get_chat().
- Use add_or_lookup_email_contact in test_setup_contact_ex.
- Use vCards more in Python tests.
- Use TestContextManager in more tests.
- Use vCards to create contacts in more Rust tests.
- Set chat name multiple times in a row.
- Online test for renaming the group multiple times.
## [1.157.3] - 2025-03-19
### API-Changes
- jsonrpc: Add `copy_to_blob_dir` api ([#6660](https://github.com/chatmail/core/pull/6660)).
- Add "delete_for_all" function in json-rpc ([#6672](https://github.com/chatmail/core/pull/6672)).
- Sketch add_transport_from_qr(), add_transport(), list_transports(), delete_transport() APIs ([#6589](https://github.com/chatmail/core/pull/6589)).
### Build system
- Remove websocket support from deltachat-jsonrpc.
- Remove encoded-words dependency.
### Fixes
- Never send empty `To:` header ([#6663](https://github.com/chatmail/core/pull/6663)).
- Use protected `Date` header for signed messages.
- Fix setting up a profile and immediately transferring to a second device ([#6657](https://github.com/chatmail/core/pull/6657)).
- Don't SMTP-send self-only messages if DeleteServerAfter is "immediate" ([#6661](https://github.com/chatmail/core/pull/6661)).
- Use protected `Date` with protected Autocrypt.
### Miscellaneous Tasks
- cargo: Bump uuid from 1.12.1 to 1.15.1.
- Update `strum` dependency.
### Refactor
- deltachat-rpc-client: Use wait_for_event() type argument.
### Tests
- Avoid creating contacts in `test_sync_{accept,block}_before_first_msg()`.
- Fix `test_no_old_msg_is_fresh` flakiness.
## [1.157.2] - 2025-03-15
### Fixes
@@ -6228,7 +6015,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[1.157.0]: https://github.com/chatmail/core/compare/v1.156.3..v1.157.0
[1.157.1]: https://github.com/chatmail/core/compare/v1.157.0..v1.157.1
[1.157.2]: https://github.com/chatmail/core/compare/v1.157.1..v1.157.2
[1.157.3]: https://github.com/chatmail/core/compare/v1.157.2..v1.157.3
[1.158.0]: https://github.com/chatmail/core/compare/v1.157.3..v1.158.0
[1.159.0]: https://github.com/chatmail/core/compare/v1.158.0..v1.159.0
[1.159.1]: https://github.com/chatmail/core/compare/v1.159.0..v1.159.1

427
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.159.1"
version = "1.157.2"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.82"
rust-version = "1.81"
repository = "https://github.com/chatmail/core"
[profile.dev]
@@ -41,7 +41,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.2"
async-channel = { workspace = true }
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.10.3", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
@@ -57,11 +57,11 @@ futures-lite = { workspace = true }
futures = { workspace = true }
hex = "0.4.0"
hickory-resolver = "=0.25.0-alpha.5"
http-body-util = "0.1.3"
http-body-util = "0.1.2"
humansize = "2"
hyper = "1"
hyper-util = "0.1.11"
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
hyper-util = "0.1.10"
image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.33", default-features = false, features = ["net"] }
iroh = { version = "0.33", default-features = false }
kamadak-exif = "0.6.1"
@@ -72,6 +72,7 @@ mime = "0.3.17"
num_cpus = "1.16"
num-derive = "0.4"
num-traits = { workspace = true }
once_cell = { workspace = true }
parking_lot = "0.12"
percent-encoding = "2.3"
pgp = { version = "0.15.0", default-features = false }
@@ -96,7 +97,7 @@ smallvec = "1.14.0"
strum = "0.27"
strum_macros = "0.27"
tagger = "4.3.4"
textwrap = "0.16.2"
textwrap = "0.16.1"
thiserror = { workspace = true }
tokio-io-timeout = "1.2.0"
tokio-rustls = { version = "0.26.2", default-features = false }
@@ -108,7 +109,7 @@ toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
webpki-roots = "0.26.8"
blake3 = "1.8.0"
blake3 = "1.6.1"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
@@ -184,16 +185,17 @@ log = "0.4"
mailparse = "0.16.1"
nu-ansi-term = "0.46"
num-traits = "0.2"
once_cell = "1.20.2"
rand = "0.8"
regex = "1.10"
rusqlite = "0.32"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.19.1"
tempfile = "3.14.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.14"
tokio-util = "0.7.13"
tracing-subscriber = "0.3"
yerpc = "0.6.2"

View File

@@ -1,5 +1,5 @@
<p align="center">
<img alt="Chatmail logo" src="https://github.com/user-attachments/assets/25742da7-a837-48cd-a503-b303af55f10d" width="300" style="float:middle;" />
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
</p>
<p align="center">
@@ -11,31 +11,9 @@
</a>
</p>
The chatmail core library implements low-level network and encryption protocols,
integrated by many chat bots and higher level applications,
allowing to securely participate in the globally scaled e-mail server network.
We provide reproducibly-built `deltachat-rpc-server` static binaries
that offer a stdio-based high-level JSON-RPC API for instant messaging purposes.
The following protocols are handled without requiring API users to know much about them:
- secure TLS setup with DNS caching and shadowsocks/proxy support
- robust [SMTP](https://github.com/chatmail/async-imap)
and [IMAP](https://github.com/chatmail/async-smtp) handling
- safe and interoperable [MIME parsing](https://github.com/staktrace/mailparse)
and [MIME building](https://github.com/stalwartlabs/mail-builder).
- security-audited end-to-end encryption with [rPGP](https://github.com/rpgp/rpgp)
and [Autocrypt and SecureJoin protocols](https://securejoin.rtfd.io)
- ephemeral [Peer-to-Peer networking using Iroh](https://iroh.computer) for multi-device setup and
[webxdc realtime data](https://delta.chat/en/2024-11-20-webxdc-realtime).
- a simulation- and real-world tested [P2P group membership
protocol without requiring server state](https://github.com/chatmail/models/tree/main/group-membership).
<p align="center">
The core library for Delta Chat, written in Rust
</p>
## Installing Rust and Cargo

View File

@@ -9,6 +9,7 @@ license = "MPL-2.0"
[dependencies]
anyhow = { workspace = true }
once_cell = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
chrono = { workspace = true, features = ["alloc", "clock", "std"] }

View File

@@ -29,14 +29,202 @@
use std::fmt;
use std::ops::Deref;
use std::sync::LazyLock;
use anyhow::bail;
use anyhow::Context as _;
use anyhow::Result;
use chrono::{DateTime, NaiveDateTime};
use once_cell::sync::Lazy;
use regex::Regex;
mod vcard;
pub use vcard::{make_vcard, parse_vcard, VcardContact};
#[derive(Debug)]
/// A Contact, as represented in a VCard.
pub struct VcardContact {
/// The email address, vcard property `email`
pub addr: String,
/// This must be the name authorized by the contact itself, not a locally given name. Vcard
/// property `fn`. Can be empty, one should use `display_name()` to obtain the display name.
pub authname: String,
/// The contact's public PGP key in Base64, vcard property `key`
pub key: Option<String>,
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
pub profile_image: Option<String>,
/// The timestamp when the vcard was created / last updated, vcard property `rev`
pub timestamp: Result<i64>,
}
impl VcardContact {
/// Returns the contact's display name.
pub fn display_name(&self) -> &str {
match self.authname.is_empty() {
false => &self.authname,
true => &self.addr,
}
}
}
/// Returns a vCard containing given contacts.
///
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
pub fn make_vcard(contacts: &[VcardContact]) -> String {
fn format_timestamp(c: &VcardContact) -> Option<String> {
let timestamp = *c.timestamp.as_ref().ok()?;
let datetime = DateTime::from_timestamp(timestamp, 0)?;
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
}
let mut res = "".to_string();
for c in contacts {
let addr = &c.addr;
let display_name = c.display_name();
res += &format!(
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
EMAIL:{addr}\r\n\
FN:{display_name}\r\n"
);
if let Some(key) = &c.key {
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
}
if let Some(profile_image) = &c.profile_image {
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
}
if let Some(timestamp) = format_timestamp(c) {
res += &format!("REV:{timestamp}\r\n");
}
res += "END:VCARD\r\n";
}
res
}
/// Parses `VcardContact`s from a given `&str`.
pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
let start_of_s = s.get(..prefix.len())?;
if start_of_s.eq_ignore_ascii_case(prefix) {
s.get(prefix.len()..)
} else {
None
}
}
fn vcard_property<'a>(s: &'a str, property: &str) -> Option<&'a str> {
let remainder = remove_prefix(s, property)?;
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
// then `remainder` is now `;TYPE=work:alice@example.com`
// Note: This doesn't handle the case where there are quotes around a colon,
// like `NAME;Foo="Some quoted text: that contains a colon":value`.
// This could be improved in the future, but for now, the parsing is good enough.
let (params, value) = remainder.split_once(':')?;
// In the example from above, `params` is now `;TYPE=work`
// and `value` is now `alice@example.com`
if params
.chars()
.next()
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
.is_some()
{
// `s` started with `property`, but the next character after it was not punctuation,
// so this line's property is actually something else
return None;
}
Some(value)
}
fn parse_datetime(datetime: &str) -> Result<i64> {
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
// is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses
// ISO.8601, but fails to parse any of the examples given.
// So, instead just parse using a format string.
// Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500.
let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") {
Ok(datetime) => datetime.timestamp(),
// Parses 19961022T140000.
Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") {
Ok(datetime) => datetime
.and_local_timezone(chrono::offset::Local)
.single()
.context("Could not apply local timezone to parsed date and time")?
.timestamp(),
Err(_) => return Err(e.into()),
},
};
Ok(timestamp)
}
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
static NEWLINE_AND_SPACE_OR_TAB: Lazy<Regex> = Lazy::new(|| Regex::new("\r?\n[\t ]").unwrap());
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
let mut lines = unfolded_lines.lines().peekable();
let mut contacts = Vec::new();
while lines.peek().is_some() {
// Skip to the start of the vcard:
for line in lines.by_ref() {
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
break;
}
}
let mut display_name = None;
let mut addr = None;
let mut key = None;
let mut photo = None;
let mut datetime = None;
for mut line in lines.by_ref() {
if let Some(remainder) = remove_prefix(line, "item1.") {
// Remove the group name, if the group is called "item1".
// If necessary, we can improve this to also remove groups that are called something different that "item1".
//
// Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos.
line = remainder;
}
if let Some(email) = vcard_property(line, "email") {
addr.get_or_insert(email);
} else if let Some(name) = vcard_property(line, "fn") {
display_name.get_or_insert(name);
} else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:")
.or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:"))
.or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,"))
.or_else(|| remove_prefix(line, "KEY;PREF=1:data:application/pgp-keys;base64,"))
{
key.get_or_insert(k);
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:"))
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=b;TYPE=JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=BASE64:"))
.or_else(|| remove_prefix(line, "PHOTO:data:image/jpeg;base64,"))
{
photo.get_or_insert(p);
} else if let Some(rev) = vcard_property(line, "rev") {
datetime.get_or_insert(rev);
} else if line.eq_ignore_ascii_case("END:VCARD") {
let (authname, addr) =
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
contacts.push(VcardContact {
authname,
addr,
key: key.map(|s| s.to_string()),
profile_image: photo.map(|s| s.to_string()),
timestamp: datetime
.context("No timestamp in vcard")
.and_then(parse_datetime),
});
break;
}
}
}
contacts
}
/// Valid contact address.
#[derive(Debug, Clone)]
@@ -88,8 +276,7 @@ impl rusqlite::types::ToSql for ContactAddress {
/// - Removes special characters from the name, see [`sanitize_name()`]
/// - Removes the name if it is equal to the address by setting it to ""
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
static ADDR_WITH_NAME_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new("(.*)<(.*)>").unwrap());
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.is_empty() {
@@ -291,8 +478,148 @@ impl rusqlite::types::ToSql for EmailAddress {
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use super::*;
#[test]
fn test_vcard_thunderbird() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:'Alice Mueller'
EMAIL;PREF=1:alice.mueller@posteo.de
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
END:VCARD
BEGIN:VCARD
VERSION:4.0
FN:'bobzzz@freenet.de'
EMAIL;PREF=1:bobzzz@freenet.de
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
END:VCARD
",
);
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
assert_eq!(contacts[0].authname, "Alice Mueller".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
assert_eq!(contacts[1].authname, "".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert!(contacts[1].timestamp.is_err());
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_simple_example() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:Alice Wonderland
N:Wonderland;Alice;;;Ms.
GENDER:W
EMAIL;TYPE=work:alice@example.com
KEY;TYPE=PGP;ENCODING=b:[base64-data]
REV:20240418T184242Z
END:VCARD",
);
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
assert_eq!(contacts[0].profile_image, None);
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
assert_eq!(contacts.len(), 1);
}
#[test]
fn test_vcard_with_trailing_newline() {
let contacts = parse_vcard(
"BEGIN:VCARD\r
VERSION:4.0\r
FN:Alice Wonderland\r
N:Wonderland;Alice;;;Ms.\r
GENDER:W\r
EMAIL;TYPE=work:alice@example.com\r
KEY;TYPE=PGP;ENCODING=b:[base64-data]\r
REV:20240418T184242Z\r
END:VCARD\r
\r",
);
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
assert_eq!(contacts[0].profile_image, None);
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
assert_eq!(contacts.len(), 1);
}
#[test]
fn test_make_and_parse_vcard() {
let contacts = [
VcardContact {
addr: "alice@example.org".to_string(),
authname: "Alice Wonderland".to_string(),
key: Some("[base64-data]".to_string()),
profile_image: Some("image in Base64".to_string()),
timestamp: Ok(1713465762),
},
VcardContact {
addr: "bob@example.com".to_string(),
authname: "".to_string(),
key: None,
profile_image: None,
timestamp: Ok(0),
},
];
let items = [
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
EMAIL:alice@example.org\r\n\
FN:Alice Wonderland\r\n\
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
REV:20240418T184242Z\r\n\
END:VCARD\r\n",
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
EMAIL:bob@example.com\r\n\
FN:bob@example.com\r\n\
REV:19700101T000000Z\r\n\
END:VCARD\r\n",
];
let mut expected = "".to_string();
for len in 0..=contacts.len() {
let contacts = &contacts[0..len];
let vcard = make_vcard(contacts);
if len > 0 {
expected += items[len - 1];
}
assert_eq!(vcard, expected);
let parsed = parse_vcard(&vcard);
assert_eq!(parsed.len(), contacts.len());
for i in 0..parsed.len() {
assert_eq!(parsed[i].addr, contacts[i].addr);
assert_eq!(parsed[i].authname, contacts[i].authname);
assert_eq!(parsed[i].key, contacts[i].key);
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
assert_eq!(
parsed[i].timestamp.as_ref().unwrap(),
contacts[i].timestamp.as_ref().unwrap()
);
}
}
}
#[test]
fn test_contact_address() -> Result<()> {
let alice_addr = "alice@example.org";
@@ -339,6 +666,112 @@ mod tests {
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
}
#[test]
fn test_vcard_android() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
TEL;CELL:+1-234-567-890
EMAIL;HOME:bob@example.org
END:VCARD
BEGIN:VCARD
VERSION:2.1
N:;Alice;;;
FN:Alice
EMAIL;HOME:alice@example.org
END:VCARD
",
);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
assert_eq!(contacts[1].authname, "Alice".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_local_datetime() {
let contacts = parse_vcard(
"BEGIN:VCARD\n\
VERSION:4.0\n\
FN:Alice Wonderland\n\
EMAIL;TYPE=work:alice@example.org\n\
REV:20240418T184242\n\
END:VCARD",
);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(
*contacts[0].timestamp.as_ref().unwrap(),
chrono::offset::Local
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
.unwrap()
.timestamp()
);
}
#[test]
fn test_vcard_with_base64_avatar() {
// This is not an actual base64-encoded avatar, it's just to test the parsing.
// This one is Android-like.
let vcard0 = "BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
EMAIL;HOME:bob@example.org
PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
END:VCARD
";
// This one is DOS-like.
let vcard1 = vcard0.replace('\n', "\r\n");
for vcard in [vcard0, vcard1.as_str()] {
let contacts = parse_vcard(vcard);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
}
}
#[test]
fn test_protonmail_vcard() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN;PREF=1:Alice Wonderland
UID:proton-web-03747582-328d-38dc-5ddd-000000000000
ITEM1.EMAIL;PREF=1:alice@example.org
ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
ITEM1.X-PM-ENCRYPT:true
ITEM1.X-PM-SIGN:true
END:VCARD",
);
assert_eq!(contacts.len(), 1);
assert_eq!(&contacts[0].addr, "alice@example.org");
assert_eq!(&contacts[0].authname, "Alice Wonderland");
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[0].profile_image, None);
}
#[test]
fn test_sanitize_name() {
assert_eq!(&sanitize_name(" hello world "), "hello world");

View File

@@ -1,232 +0,0 @@
use std::sync::LazyLock;
use anyhow::Context as _;
use anyhow::Result;
use chrono::DateTime;
use chrono::NaiveDateTime;
use regex::Regex;
use crate::sanitize_name_and_addr;
#[derive(Debug)]
/// A Contact, as represented in a VCard.
pub struct VcardContact {
/// The email address, vcard property `email`
pub addr: String,
/// This must be the name authorized by the contact itself, not a locally given name. Vcard
/// property `fn`. Can be empty, one should use `display_name()` to obtain the display name.
pub authname: String,
/// The contact's public PGP key in Base64, vcard property `key`
pub key: Option<String>,
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
pub profile_image: Option<String>,
/// The timestamp when the vcard was created / last updated, vcard property `rev`
pub timestamp: Result<i64>,
}
impl VcardContact {
/// Returns the contact's display name.
pub fn display_name(&self) -> &str {
match self.authname.is_empty() {
false => &self.authname,
true => &self.addr,
}
}
}
/// Returns a vCard containing given contacts.
///
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
pub fn make_vcard(contacts: &[VcardContact]) -> String {
fn format_timestamp(c: &VcardContact) -> Option<String> {
let timestamp = *c.timestamp.as_ref().ok()?;
let datetime = DateTime::from_timestamp(timestamp, 0)?;
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
}
let mut res = "".to_string();
for c in contacts {
let addr = &c.addr;
let display_name = c.display_name();
res += &format!(
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
EMAIL:{addr}\r\n\
FN:{display_name}\r\n"
);
if let Some(key) = &c.key {
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
}
if let Some(profile_image) = &c.profile_image {
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
}
if let Some(timestamp) = format_timestamp(c) {
res += &format!("REV:{timestamp}\r\n");
}
res += "END:VCARD\r\n";
}
res
}
/// Parses `VcardContact`s from a given `&str`.
pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
let start_of_s = s.get(..prefix.len())?;
if start_of_s.eq_ignore_ascii_case(prefix) {
s.get(prefix.len()..)
} else {
None
}
}
/// Returns (parameters, value) tuple.
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
let remainder = remove_prefix(line, property)?;
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
// then `remainder` is now `;TYPE=work:alice@example.com`
// Note: This doesn't handle the case where there are quotes around a colon,
// like `NAME;Foo="Some quoted text: that contains a colon":value`.
// This could be improved in the future, but for now, the parsing is good enough.
let (mut params, value) = remainder.split_once(':')?;
// In the example from above, `params` is now `;TYPE=work`
// and `value` is now `alice@example.com`
if params
.chars()
.next()
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
.is_some()
{
// `s` started with `property`, but the next character after it was not punctuation,
// so this line's property is actually something else
return None;
}
if let Some(p) = remove_prefix(params, ";") {
params = p;
}
if let Some(p) = remove_prefix(params, "PREF=1") {
params = p;
}
Some((params, value))
}
fn base64_key(line: &str) -> Option<&str> {
let (params, value) = vcard_property(line, "key")?;
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
{
return Some(value);
}
if let Some(value) = remove_prefix(value, "data:application/pgp-keys;base64,")
.or_else(|| remove_prefix(value, r"data:application/pgp-keys;base64\,"))
{
return Some(value);
}
None
}
fn base64_photo(line: &str) -> Option<&str> {
let (params, value) = vcard_property(line, "photo")?;
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
|| params.eq_ignore_ascii_case("ENCODING=b;TYPE=JPEG")
|| params.eq_ignore_ascii_case("ENCODING=BASE64;TYPE=JPEG")
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=BASE64")
{
return Some(value);
}
if let Some(value) = remove_prefix(value, "data:image/jpeg;base64,")
.or_else(|| remove_prefix(value, r"data:image/jpeg;base64\,"))
{
return Some(value);
}
None
}
fn parse_datetime(datetime: &str) -> Result<i64> {
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
// is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses
// ISO.8601, but fails to parse any of the examples given.
// So, instead just parse using a format string.
// Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500.
let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") {
Ok(datetime) => datetime.timestamp(),
// Parses 19961022T140000.
Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") {
Ok(datetime) => datetime
.and_local_timezone(chrono::offset::Local)
.single()
.context("Could not apply local timezone to parsed date and time")?
.timestamp(),
Err(_) => return Err(e.into()),
},
};
Ok(timestamp)
}
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
static NEWLINE_AND_SPACE_OR_TAB: LazyLock<Regex> =
LazyLock::new(|| Regex::new("\r?\n[\t ]").unwrap());
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
let mut lines = unfolded_lines.lines().peekable();
let mut contacts = Vec::new();
while lines.peek().is_some() {
// Skip to the start of the vcard:
for line in lines.by_ref() {
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
break;
}
}
let mut display_name = None;
let mut addr = None;
let mut key = None;
let mut photo = None;
let mut datetime = None;
for mut line in lines.by_ref() {
if let Some(remainder) = remove_prefix(line, "item1.") {
// Remove the group name, if the group is called "item1".
// If necessary, we can improve this to also remove groups that are called something different that "item1".
//
// Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos.
line = remainder;
}
if let Some((_params, email)) = vcard_property(line, "email") {
addr.get_or_insert(email);
} else if let Some((_params, name)) = vcard_property(line, "fn") {
display_name.get_or_insert(name);
} else if let Some(k) = base64_key(line) {
key.get_or_insert(k);
} else if let Some(p) = base64_photo(line) {
photo.get_or_insert(p);
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
datetime.get_or_insert(rev);
} else if line.eq_ignore_ascii_case("END:VCARD") {
let (authname, addr) =
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
contacts.push(VcardContact {
authname,
addr,
key: key.map(|s| s.to_string()),
profile_image: photo.map(|s| s.to_string()),
timestamp: datetime
.context("No timestamp in vcard")
.and_then(parse_datetime),
});
break;
}
}
}
contacts
}
#[cfg(test)]
mod vcard_tests;

View File

@@ -1,274 +0,0 @@
use chrono::TimeZone as _;
use super::*;
#[test]
fn test_vcard_thunderbird() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:'Alice Mueller'
EMAIL;PREF=1:alice.mueller@posteo.de
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
END:VCARD
BEGIN:VCARD
VERSION:4.0
FN:'bobzzz@freenet.de'
EMAIL;PREF=1:bobzzz@freenet.de
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
END:VCARD
",
);
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
assert_eq!(contacts[0].authname, "Alice Mueller".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
assert_eq!(contacts[1].authname, "".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert!(contacts[1].timestamp.is_err());
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_simple_example() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:Alice Wonderland
N:Wonderland;Alice;;;Ms.
GENDER:W
EMAIL;TYPE=work:alice@example.com
KEY;TYPE=PGP;ENCODING=b:[base64-data]
REV:20240418T184242Z
END:VCARD",
);
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
assert_eq!(contacts[0].profile_image, None);
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
assert_eq!(contacts.len(), 1);
}
#[test]
fn test_vcard_with_trailing_newline() {
let contacts = parse_vcard(
"BEGIN:VCARD\r
VERSION:4.0\r
FN:Alice Wonderland\r
N:Wonderland;Alice;;;Ms.\r
GENDER:W\r
EMAIL;TYPE=work:alice@example.com\r
KEY;TYPE=PGP;ENCODING=b:[base64-data]\r
REV:20240418T184242Z\r
END:VCARD\r
\r",
);
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
assert_eq!(contacts[0].profile_image, None);
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
assert_eq!(contacts.len(), 1);
}
#[test]
fn test_make_and_parse_vcard() {
let contacts = [
VcardContact {
addr: "alice@example.org".to_string(),
authname: "Alice Wonderland".to_string(),
key: Some("[base64-data]".to_string()),
profile_image: Some("image in Base64".to_string()),
timestamp: Ok(1713465762),
},
VcardContact {
addr: "bob@example.com".to_string(),
authname: "".to_string(),
key: None,
profile_image: None,
timestamp: Ok(0),
},
];
let items = [
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
EMAIL:alice@example.org\r\n\
FN:Alice Wonderland\r\n\
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
REV:20240418T184242Z\r\n\
END:VCARD\r\n",
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
EMAIL:bob@example.com\r\n\
FN:bob@example.com\r\n\
REV:19700101T000000Z\r\n\
END:VCARD\r\n",
];
let mut expected = "".to_string();
for len in 0..=contacts.len() {
let contacts = &contacts[0..len];
let vcard = make_vcard(contacts);
if len > 0 {
expected += items[len - 1];
}
assert_eq!(vcard, expected);
let parsed = parse_vcard(&vcard);
assert_eq!(parsed.len(), contacts.len());
for i in 0..parsed.len() {
assert_eq!(parsed[i].addr, contacts[i].addr);
assert_eq!(parsed[i].authname, contacts[i].authname);
assert_eq!(parsed[i].key, contacts[i].key);
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
assert_eq!(
parsed[i].timestamp.as_ref().unwrap(),
contacts[i].timestamp.as_ref().unwrap()
);
}
}
}
#[test]
fn test_vcard_android() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
TEL;CELL:+1-234-567-890
EMAIL;HOME:bob@example.org
END:VCARD
BEGIN:VCARD
VERSION:2.1
N:;Alice;;;
FN:Alice
EMAIL;HOME:alice@example.org
END:VCARD
",
);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
assert_eq!(contacts[1].authname, "Alice".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_local_datetime() {
let contacts = parse_vcard(
"BEGIN:VCARD\n\
VERSION:4.0\n\
FN:Alice Wonderland\n\
EMAIL;TYPE=work:alice@example.org\n\
REV:20240418T184242\n\
END:VCARD",
);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(
*contacts[0].timestamp.as_ref().unwrap(),
chrono::offset::Local
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
.unwrap()
.timestamp()
);
}
#[test]
fn test_vcard_with_base64_avatar() {
// This is not an actual base64-encoded avatar, it's just to test the parsing.
// This one is Android-like.
let vcard0 = "BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
EMAIL;HOME:bob@example.org
PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
END:VCARD
";
// This one is DOS-like.
let vcard1 = vcard0.replace('\n', "\r\n");
for vcard in [vcard0, vcard1.as_str()] {
let contacts = parse_vcard(vcard);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
}
}
#[test]
fn test_protonmail_vcard() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN;PREF=1:Alice Wonderland
UID:proton-web-03747582-328d-38dc-5ddd-000000000000
ITEM1.EMAIL;PREF=1:alice@example.org
ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
ITEM1.X-PM-ENCRYPT:true
ITEM1.X-PM-SIGN:true
END:VCARD",
);
assert_eq!(contacts.len(), 1);
assert_eq!(&contacts[0].addr, "alice@example.org");
assert_eq!(&contacts[0].authname, "Alice Wonderland");
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[0].profile_image, None);
}
/// Proton at some point slightly changed the format of their vcards
#[test]
fn test_protonmail_vcard2() {
let contacts = parse_vcard(
r"BEGIN:VCARD
VERSION:4.0
FN;PREF=1:Alice
PHOTO;PREF=1:data:image/jpeg;base64,/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z
REV:Invalid Date
ITEM1.EMAIL;PREF=1:alice@example.org
KEY;PREF=1:data:application/pgp-keys;base64,xsaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==
UID:proton-web-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
END:VCARD",
);
assert_eq!(contacts.len(), 1);
assert_eq!(&contacts[0].addr, "alice@example.org");
assert_eq!(&contacts[0].authname, "Alice");
assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==");
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.159.1"
version = "1.157.2"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -24,6 +24,7 @@ tokio = { workspace = true, features = ["rt-multi-thread"] }
anyhow = { workspace = true }
thiserror = { workspace = true }
rand = { workspace = true }
once_cell = { workspace = true }
yerpc = { workspace = true, features = ["anyhow_expose"] }
[features]

View File

@@ -2162,29 +2162,6 @@ uint32_t dc_create_contact (dc_context_t* context, const char*
int dc_add_address_book (dc_context_t* context, const char* addr_book);
/**
* Make a vCard.
*
* @memberof dc_context_t
* @param context The context object.
* @param contact_id The ID of the contact to make the vCard of.
* @return vCard, must be released using dc_str_unref() after usage.
*/
char* dc_make_vcard (dc_context_t* context, uint32_t contact_id);
/**
* Import a vCard.
*
* @memberof dc_context_t
* @param context The context object.
* @param vcard vCard contents.
* @return Returns the IDs of the contacts in the order they appear in the vCard.
* Must be dc_array_unref()'d after usage.
*/
dc_array_t* dc_import_vcard (dc_context_t* context, const char* vcard);
/**
* Returns known and unblocked contacts.
*
@@ -4506,11 +4483,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
* UIs can display e.g. an icon based upon the type.
*
* Currently, the following types are defined:
* - DC_INFO_GROUP_NAME_CHANGED (2) - "Group name changd from OLD to BY by CONTACT"
* - DC_INFO_GROUP_IMAGE_CHANGED (3) - "Group image changd by CONTACT"
* - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT"
* - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT"
* - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT"
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
@@ -4518,10 +4490,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
*
* For the messages that refer to a CONTACT,
* dc_msg_get_info_contact_id() returns the contact ID.
* The UI should open the contact's profile when tapping the info message.
*
* Even when you display an icon,
* you should still display the text of the informational message using dc_msg_get_text()
*
@@ -4534,29 +4502,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
int dc_msg_get_info_type (const dc_msg_t* msg);
/**
* Return the contact ID of the profile to open when tapping the info message.
*
* - For DC_INFO_MEMBER_ADDED_TO_GROUP and DC_INFO_MEMBER_REMOVED_FROM_GROUP,
* this is the contact being added/removed.
* The contact that did the adding/removal is usually only a tap away
* (as introducer and/or atop of the memberlist),
* and usually more known anyways.
* - For DC_INFO_GROUP_NAME_CHANGED, DC_INFO_GROUP_IMAGE_CHANGED and DC_INFO_EPHEMERAL_TIMER_CHANGED
* this is the contact who did the change.
*
* No need to check additionally for dc_msg_get_info_type(),
* unless you e.g. want to show the info message in another style.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return If the info message refers to a contact,
* this contact ID or DC_CONTACT_ID_SELF is returned.
* Otherwise 0.
*/
uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
// DC_INFO* uses the same values as SystemMessage in rust-land
#define DC_INFO_UNKNOWN 0
#define DC_INFO_GROUP_NAME_CHANGED 2
@@ -6912,12 +6857,12 @@ void dc_event_unref(dc_event_t* event);
/// "Autocrypt Setup Message"
///
/// @deprecated 2025-04
/// Used in subjects of outgoing Autocrypt Setup Messages.
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
/// "This is the Autocrypt Setup Message, open it in a compatible client to use your setup"
///
/// @deprecated 2025-04
/// Used as message text of outgoing Autocrypt Setup Messages.
#define DC_STR_AC_SETUP_MSG_BODY 43
/// "Cannot login as %1$s."
@@ -7592,13 +7537,8 @@ void dc_event_unref(dc_event_t* event);
/// "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
///
/// @deprecated 2025-03
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
/// "That seems to take longer, maybe the contact or you are offline. However, the process continues in background, you can do something else…"
///
/// Used as info message.
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
/// "Contact". Deprecated, currently unused.
#define DC_STR_CONTACT 200

View File

@@ -18,7 +18,7 @@ use std::future::Future;
use std::ops::Deref;
use std::ptr;
use std::str::FromStr;
use std::sync::{Arc, LazyLock};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
@@ -38,6 +38,7 @@ use deltachat::{accounts::Accounts, log::LogExt};
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use num_traits::{FromPrimitive, ToPrimitive};
use once_cell::sync::Lazy;
use rand::Rng;
use tokio::runtime::Runtime;
use tokio::sync::RwLock;
@@ -67,8 +68,7 @@ const DC_GCM_INFO_ONLY: u32 = 0x02;
/// Struct representing the deltachat context.
pub type dc_context_t = Context;
static RT: LazyLock<Runtime> =
LazyLock::new(|| Runtime::new().expect("unable to create tokio runtime"));
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
fn block_on<T>(fut: T) -> T::Output
where
@@ -536,7 +536,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::IncomingReaction { .. } => 2002,
EventType::IncomingWebxdcNotify { .. } => 2003,
EventType::IncomingMsg { .. } => 2005,
EventType::IncomingMsgBunch => 2006,
EventType::IncomingMsgBunch { .. } => 2006,
EventType::MsgsNoticed { .. } => 2008,
EventType::MsgDelivered { .. } => 2010,
EventType::MsgFailed { .. } => 2012,
@@ -594,7 +594,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::ConfigSynced { .. }
| EventType::IncomingMsgBunch
| EventType::IncomingMsgBunch { .. }
| EventType::ErrorSelfNotInGroup(_)
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
@@ -669,7 +669,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::MsgsNoticed(_)
| EventType::ConnectivityChanged
| EventType::WebxdcInstanceDeleted { .. }
| EventType::IncomingMsgBunch
| EventType::IncomingMsgBunch { .. }
| EventType::SelfavatarChanged
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
@@ -771,7 +771,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::AccountsBackgroundFetchDone
| EventType::ChatEphemeralTimerModified { .. }
| EventType::ChatDeleted { .. }
| EventType::IncomingMsgBunch
| EventType::IncomingMsgBunch { .. }
| EventType::ChatlistItemChanged { .. }
| EventType::ChatlistChanged
| EventType::AccountsChanged
@@ -2170,48 +2170,6 @@ pub unsafe extern "C" fn dc_add_address_book(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_make_vcard(
context: *mut dc_context_t,
contact_id: u32,
) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_make_vcard()");
return ptr::null_mut();
}
let ctx = &*context;
let contact_id = ContactId::new(contact_id);
block_on(contact::make_vcard(ctx, &[contact_id]))
.unwrap_or_log_default(ctx, "dc_make_vcard failed")
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_import_vcard(
context: *mut dc_context_t,
vcard: *const libc::c_char,
) -> *mut dc_array::dc_array_t {
if context.is_null() || vcard.is_null() {
eprintln!("ignoring careless call to dc_import_vcard()");
return ptr::null_mut();
}
let ctx = &*context;
match block_on(contact::import_vcard(ctx, &to_string_lossy(vcard)))
.context("dc_import_vcard failed")
.log_err(ctx)
{
Ok(contact_ids) => Box::into_raw(Box::new(dc_array_t::from(
contact_ids
.iter()
.map(|id| id.to_u32())
.collect::<Vec<u32>>(),
))),
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_contacts(
context: *mut dc_context_t,
@@ -3772,20 +3730,6 @@ pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int
ffi_msg.message.get_info_type() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_info_contact_id(msg: *mut dc_msg_t) -> u32 {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_info_contact_id()");
return 0;
}
let ffi_msg = &*msg;
let context = &*ffi_msg.context;
block_on(ffi_msg.message.get_info_contact_id(context))
.unwrap_or_default()
.map(|id| id.to_u32())
.unwrap_or_default()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.159.1"
version = "1.157.2"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -39,7 +39,6 @@ use deltachat::{imex, info};
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
use types::login_param::EnteredLoginParam;
use walkdir::WalkDir;
use yerpc::rpc;
@@ -227,9 +226,8 @@ impl CommandApi {
/// Get a list of all configured accounts.
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
let mut accounts = Vec::new();
let accounts_lock = self.accounts.read().await;
for id in accounts_lock.get_all() {
let context_option = accounts_lock.get_account(id);
for id in self.accounts.read().await.get_all() {
let context_option = self.accounts.read().await.get_account(id);
if let Some(ctx) = context_option {
accounts.push(Account::from_context(&ctx, id).await?)
}
@@ -327,12 +325,8 @@ impl CommandApi {
.get_config_bool(deltachat::config::Config::ProxyEnabled)
.await?;
let provider_info = get_provider_info(
&ctx,
email.split('@').next_back().unwrap_or(""),
proxy_enabled,
)
.await;
let provider_info =
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), proxy_enabled).await;
Ok(ProviderInfo::from_dc_type(provider_info))
}
@@ -354,8 +348,8 @@ impl CommandApi {
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
}
/// Copy file to blob dir.
async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result<PathBuf> {
/// Copy file to blobdir.
async fn copy_to_blobdir(&self, account_id: u32, path: String) -> Result<PathBuf> {
let ctx = self.get_context(account_id).await?;
let file = Path::new(&path);
Ok(BlobObject::create_and_deduplicate(&ctx, file, file)?.to_abs_path())
@@ -437,9 +431,6 @@ impl CommandApi {
/// Configures this account with the currently set parameters.
/// Setup the credential config before calling this.
///
/// Deprecated as of 2025-02; use `add_transport_from_qr()`
/// or `add_transport()` instead.
async fn configure(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
@@ -454,69 +445,6 @@ impl CommandApi {
Ok(())
}
/// Configures a new email account using the provided parameters
/// and adds it as a transport.
///
/// If the email address is the same as an existing transport,
/// then this existing account will be reconfigured instead of a new one being added.
///
/// This function stops and starts IO as needed.
///
/// Usually it will be enough to only set `addr` and `password`,
/// and all the other settings will be autoconfigured.
///
/// During configuration, ConfigureProgress events are emitted;
/// they indicate a successful configuration as well as errors
/// and may be used to create a progress bar.
/// This function will return after configuration is finished.
///
/// If configuration is successful,
/// the working server parameters will be saved
/// and used for connecting to the server.
/// The parameters entered by the user will be saved separately
/// so that they can be prefilled when the user opens the server-configuration screen again.
///
/// See also:
/// - [Self::is_configured()] to check whether there is
/// at least one working transport.
/// - [Self::add_transport_from_qr()] to add a transport
/// from a server encoded in a QR code.
/// - [Self::list_transports()] to get a list of all configured transports.
/// - [Self::delete_transport()] to remove a transport.
async fn add_transport(&self, account_id: u32, param: EnteredLoginParam) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.add_transport(&param.try_into()?).await
}
/// Adds a new email account as a transport
/// using the server encoded in the QR code.
/// See [Self::add_transport].
async fn add_transport_from_qr(&self, account_id: u32, qr: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.add_transport_from_qr(&qr).await
}
/// Returns the list of all email accounts that are used as a transport in the current profile.
/// Use [Self::add_transport()] to add or change a transport
/// and [Self::delete_transport()] to delete a transport.
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
let ctx = self.get_context(account_id).await?;
let res = ctx
.list_transports()
.await?
.into_iter()
.map(|t| t.into())
.collect();
Ok(res)
}
/// Removes the transport with the specified email address
/// (i.e. [EnteredLoginParam::addr]).
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.delete_transport(&addr).await
}
/// Signal an ongoing process to stop.
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -1542,7 +1470,6 @@ impl CommandApi {
Ok(())
}
/// Sets display name for existing contact.
async fn change_contact_name(
&self,
account_id: u32,
@@ -1551,7 +1478,9 @@ impl CommandApi {
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
contact_id.set_name(&ctx, &name).await?;
let contact = Contact::get_by_id(&ctx, contact_id).await?;
let addr = contact.get_addr();
Contact::create(&ctx, &name, addr).await?;
Ok(())
}
@@ -1951,9 +1880,13 @@ impl CommandApi {
/// Get href from a WebxdcInfoMessage which might include a hash holding
/// information about a specific position or state in a webxdc app (optional)
async fn get_webxdc_href(&self, account_id: u32, info_msg_id: u32) -> Result<Option<String>> {
async fn get_webxdc_href(
&self,
account_id: u32,
instance_msg_id: u32,
) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(info_msg_id)).await?;
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
Ok(message.get_webxdc_href())
}
@@ -2276,37 +2209,6 @@ impl CommandApi {
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
// the better version will just be sending the current draft, though there will be probably something similar with more options to this for the corner cases like setting a marker on the map
/// Send a message to a chat.
///
/// This function returns after the message has been placed in the sending queue.
/// This does not imply that the message was really sent out yet.
/// However, from your view, you're done with the message.
/// Sooner or later it will find its way.
///
/// **Attaching files:**
///
/// Pass the file path in the `file` parameter.
/// If `file` is not in the blob directory yet,
/// it will be copied into the blob directory.
/// If you want, you can delete the file immediately after this function returns.
///
/// You can also write the attachment directly into the blob directory
/// and then pass the path as the `file` parameter;
/// this will prevent an unnecessary copying of the file.
///
/// In `filename`, you can pass the original name of the file,
/// which will then be shown in the UI.
/// in this case the current name of `file` on the filesystem will be ignored.
///
/// In order to deduplicate files that contain the same data,
/// the file will be named `<hash>.<extension>`, e.g. `ce940175885d7b78f7b7e9f1396611f.jpg`.
///
/// NOTE:
/// - This function will rename the file. To get the new file path, call `get_file()`.
/// - The file must not be modified after this function was called.
/// - Images etc. will NOT be recoded.
/// In order to recode images,
/// use `misc_set_draft` and pass `Image` as the viewtype.
#[expect(clippy::too_many_arguments)]
async fn misc_send_msg(
&self,

View File

@@ -1,203 +0,0 @@
use anyhow::Result;
use deltachat::login_param as dc;
use serde::Deserialize;
use serde::Serialize;
use yerpc::TypeDef;
/// Login parameters entered by the user.
///
/// Usually it will be enough to only set `addr` and `password`,
/// and all the other settings will be autoconfigured.
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct EnteredLoginParam {
/// Email address.
pub addr: String,
/// Password.
pub password: String,
/// Imap server hostname or IP address.
pub imap_server: Option<String>,
/// Imap server port.
pub imap_port: Option<u16>,
/// Imap socket security.
pub imap_security: Option<Socket>,
/// Imap username.
pub imap_user: Option<String>,
/// SMTP server hostname or IP address.
pub smtp_server: Option<String>,
/// SMTP server port.
pub smtp_port: Option<u16>,
/// SMTP socket security.
pub smtp_security: Option<Socket>,
/// SMTP username.
pub smtp_user: Option<String>,
/// SMTP Password.
///
/// Only needs to be specified if different than IMAP password.
pub smtp_password: Option<String>,
/// TLS options: whether to allow invalid certificates and/or
/// invalid hostnames.
/// Default: Automatic
pub certificate_checks: Option<EnteredCertificateChecks>,
/// If true, login via OAUTH2 (not recommended anymore).
/// Default: false
pub oauth2: Option<bool>,
}
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
fn from(param: dc::EnteredLoginParam) -> Self {
let imap_security: Socket = param.imap.security.into();
let smtp_security: Socket = param.smtp.security.into();
let certificate_checks: EnteredCertificateChecks = param.certificate_checks.into();
Self {
addr: param.addr,
password: param.imap.password,
imap_server: param.imap.server.into_option(),
imap_port: param.imap.port.into_option(),
imap_security: imap_security.into_option(),
imap_user: param.imap.user.into_option(),
smtp_server: param.smtp.server.into_option(),
smtp_port: param.smtp.port.into_option(),
smtp_security: smtp_security.into_option(),
smtp_user: param.smtp.user.into_option(),
smtp_password: param.smtp.password.into_option(),
certificate_checks: certificate_checks.into_option(),
oauth2: param.oauth2.into_option(),
}
}
}
impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
type Error = anyhow::Error;
fn try_from(param: EnteredLoginParam) -> Result<Self> {
Ok(Self {
addr: param.addr,
imap: dc::EnteredServerLoginParam {
server: param.imap_server.unwrap_or_default(),
port: param.imap_port.unwrap_or_default(),
security: param.imap_security.unwrap_or_default().into(),
user: param.imap_user.unwrap_or_default(),
password: param.password,
},
smtp: dc::EnteredServerLoginParam {
server: param.smtp_server.unwrap_or_default(),
port: param.smtp_port.unwrap_or_default(),
security: param.smtp_security.unwrap_or_default().into(),
user: param.smtp_user.unwrap_or_default(),
password: param.smtp_password.unwrap_or_default(),
},
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
oauth2: param.oauth2.unwrap_or_default(),
})
}
}
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum Socket {
/// Unspecified socket security, select automatically.
#[default]
Automatic,
/// TLS connection.
Ssl,
/// STARTTLS connection.
Starttls,
/// No TLS, plaintext connection.
Plain,
}
impl From<dc::Socket> for Socket {
fn from(value: dc::Socket) -> Self {
match value {
dc::Socket::Automatic => Self::Automatic,
dc::Socket::Ssl => Self::Ssl,
dc::Socket::Starttls => Self::Starttls,
dc::Socket::Plain => Self::Plain,
}
}
}
impl From<Socket> for dc::Socket {
fn from(value: Socket) -> Self {
match value {
Socket::Automatic => Self::Automatic,
Socket::Ssl => Self::Ssl,
Socket::Starttls => Self::Starttls,
Socket::Plain => Self::Plain,
}
}
}
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum EnteredCertificateChecks {
/// `Automatic` means that provider database setting should be taken.
/// If there is no provider database setting for certificate checks,
/// check certificates strictly.
#[default]
Automatic,
/// Ensure that TLS certificate is valid for the server hostname.
Strict,
/// Accept certificates that are expired, self-signed
/// or otherwise not valid for the server hostname.
AcceptInvalidCertificates,
}
impl From<dc::EnteredCertificateChecks> for EnteredCertificateChecks {
fn from(value: dc::EnteredCertificateChecks) -> Self {
match value {
dc::EnteredCertificateChecks::Automatic => Self::Automatic,
dc::EnteredCertificateChecks::Strict => Self::Strict,
dc::EnteredCertificateChecks::AcceptInvalidCertificates => {
Self::AcceptInvalidCertificates
}
dc::EnteredCertificateChecks::AcceptInvalidCertificates2 => {
Self::AcceptInvalidCertificates
}
}
}
}
impl From<EnteredCertificateChecks> for dc::EnteredCertificateChecks {
fn from(value: EnteredCertificateChecks) -> Self {
match value {
EnteredCertificateChecks::Automatic => Self::Automatic,
EnteredCertificateChecks::Strict => Self::Strict,
EnteredCertificateChecks::AcceptInvalidCertificates => Self::AcceptInvalidCertificates,
}
}
}
trait IntoOption<T> {
fn into_option(self) -> Option<T>;
}
impl<T> IntoOption<T> for T
where
T: Default + std::cmp::PartialEq,
{
fn into_option(self) -> Option<T> {
if self == T::default() {
None
} else {
Some(self)
}
}
}

View File

@@ -70,9 +70,6 @@ pub struct MessageObject {
/// when is_info is true this describes what type of system message it is
system_message_type: SystemMessageType,
/// if is_info is set, this refers to the contact profile that should be opened when the info message is tapped.
info_contact_id: Option<u32>,
duration: i32,
dimensions_height: i32,
dimensions_width: i32,
@@ -231,10 +228,6 @@ impl MessageObject {
is_forwarded: message.is_forwarded(),
is_bot: message.is_bot(),
system_message_type: message.get_info_type().into(),
info_contact_id: message
.get_info_contact_id(context)
.await?
.map(|id| id.to_u32()),
duration: message.get_duration(),
dimensions_height: message.get_height(),
@@ -680,6 +673,7 @@ pub struct MessageReadReceipt {
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageInfo {
rawtext: String,
ephemeral_timer: EphemeralTimer,
/// When message is ephemeral this contains the timestamp of the message expiry
ephemeral_timestamp: Option<i64>,
@@ -692,6 +686,7 @@ pub struct MessageInfo {
impl MessageInfo {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let rawtext = msg_id.rawtext(context).await?;
let ephemeral_timer = message.get_ephemeral_timer().into();
let ephemeral_timestamp = match message.get_ephemeral_timer() {
deltachat::ephemeral::Timer::Disabled => None,
@@ -704,6 +699,7 @@ impl MessageInfo {
let hop_info = msg_id.hop_info(context).await?;
Ok(Self {
rawtext,
ephemeral_timer,
ephemeral_timestamp,
error: message.error(),

View File

@@ -5,7 +5,6 @@ pub mod contact;
pub mod events;
pub mod http;
pub mod location;
pub mod login_param;
pub mod message;
pub mod provider_info;
pub mod qr;

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.159.1"
"version": "1.157.2"
}

View File

@@ -1,5 +1,5 @@
import { assert, expect } from "chai";
import { StdioDeltaChat as DeltaChat, DcEvent, C } from "../deltachat.js";
import { StdioDeltaChat as DeltaChat, DcEvent } from "../deltachat.js";
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
const EVENT_TIMEOUT = 20000;
@@ -80,8 +80,11 @@ describe("online tests", function () {
}
this.timeout(15000);
const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]);
const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0];
const contactId = await dc.rpc.createContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
@@ -98,18 +101,20 @@ describe("online tests", function () {
expect(messageList).have.length(1);
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
expect(message.text).equal("Hello");
expect(message.showPadlock).equal(true);
});
it("send and receive text message roundtrip", async function () {
it("send and receive text message roundtrip, encrypted on answer onwards", async function () {
if (!accountsConfigured) {
this.skip();
}
this.timeout(10000);
// send message from A to B
const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]);
const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0];
const contactId = await dc.rpc.createContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.159.1"
version = "1.157.2"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.159.1"
version = "1.157.2"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -70,11 +70,3 @@ line-length = 120
[tool.isort]
profile = "black"
[dependency-groups]
dev = [
"imap-tools",
"pytest",
"pytest-timeout",
"pytest-xdist",
]

View File

@@ -1,5 +1,4 @@
import argparse
import os
import re
import sys
from threading import Thread
@@ -90,8 +89,8 @@ def _run_cli(
help="accounts folder (default: current working directory)",
nargs="?",
)
parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL"))
parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD"))
parser.add_argument("--email", action="store", help="email address")
parser.add_argument("--password", action="store", help="password")
args = parser.parse_args(argv[1:])
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:

View File

@@ -1,6 +1,8 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING, Optional, Union
from warnings import warn
@@ -26,12 +28,9 @@ class Account:
def _rpc(self) -> "Rpc":
return self.manager.rpc
def wait_for_event(self, event_type=None) -> AttrDict:
def wait_for_event(self) -> AttrDict:
"""Wait until the next event and return it."""
while True:
next_event = AttrDict(self._rpc.wait_for_event(self.id))
if event_type is None or next_event.kind == event_type:
return next_event
return AttrDict(self._rpc.wait_for_event(self.id))
def clear_all_events(self):
"""Removes all queued-up events for a given account. Useful for tests."""
@@ -42,14 +41,14 @@ class Account:
self._rpc.remove_account(self.id)
def clone(self) -> "Account":
"""Clone given account.
This uses backup-transfer via iroh, i.e. the 'Add second device' feature."""
future = self._rpc.provide_backup.future(self.id)
qr = self._rpc.get_backup_qr(self.id)
new_account = self.manager.add_account()
new_account._rpc.get_backup(new_account.id, qr)
future()
return new_account
"""Clone given account."""
with TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
self.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
new_account = self.manager.add_account()
new_account.import_backup(files[0])
return new_account
def start_io(self) -> None:
"""Start the account I/O."""
@@ -110,17 +109,15 @@ class Account:
"""Configure an account."""
yield self._rpc.configure.future(self.id)
@futuremethod
def add_transport(self, params):
"""Add a new transport."""
yield self._rpc.add_transport.future(self.id, params)
def bring_online(self):
"""Start I/O and wait until IMAP becomes IDLE."""
self.start_io()
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
while True:
event = self.wait_for_event()
if event.kind == EventType.IMAP_INBOX_IDLE:
break
def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
Calling this method will always result in the same
@@ -128,15 +125,9 @@ class Account:
with that e-mail address, it is unblocked and its display
name is updated if specified.
:param obj: email-address, contact id or account.
:param obj: email-address or contact id.
:param name: (optional) display name for this contact.
"""
if isinstance(obj, Account):
vcard = obj.self_contact.make_vcard()
[contact] = self.import_vcard(vcard)
if name:
contact.set_name(name)
return contact
if isinstance(obj, int):
obj = Contact(self, obj)
if isinstance(obj, Contact):
@@ -157,8 +148,9 @@ class Account:
return [Contact(self, contact_id) for contact_id in contact_ids]
def create_chat(self, account: "Account") -> Chat:
"""Create a 1:1 chat with another account."""
return self.create_contact(account).create_chat()
vcard = account.self_contact.make_vcard()
[contact] = self.import_vcard(vcard)
return contact.create_chat()
def get_device_chat(self) -> Chat:
"""Return device chat."""
@@ -342,15 +334,24 @@ class Account:
def wait_for_incoming_msg_event(self):
"""Wait for incoming message event and return it."""
return self.wait_for_event(EventType.INCOMING_MSG)
while True:
event = self.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
return event
def wait_for_msgs_changed_event(self):
"""Wait for messages changed event and return it."""
return self.wait_for_event(EventType.MSGS_CHANGED)
while True:
event = self.wait_for_event()
if event.kind == EventType.MSGS_CHANGED:
return event
def wait_for_msgs_noticed_event(self):
"""Wait for messages noticed event and return it."""
return self.wait_for_event(EventType.MSGS_NOTICED)
while True:
event = self.wait_for_event()
if event.kind == EventType.MSGS_NOTICED:
return event
def wait_for_incoming_msg(self):
"""Wait for incoming message and return it.
@@ -371,7 +372,10 @@ class Account:
break
def wait_for_reactions_changed(self):
return self.wait_for_event(EventType.REACTIONS_CHANGED)
while True:
event = self.wait_for_event()
if event.kind == EventType.REACTIONS_CHANGED:
return event
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""

View File

@@ -48,7 +48,6 @@ class EventType(str, Enum):
MSG_READ = "MsgRead"
MSG_DELETED = "MsgDeleted"
CHAT_MODIFIED = "ChatModified"
CHAT_DELETED = "ChatDeleted"
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
CONTACTS_CHANGED = "ContactsChanged"
LOCATION_CHANGED = "LocationChanged"

View File

@@ -64,10 +64,6 @@ class Message:
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
def get_info(self) -> str:
"""Return message info."""
return self._rpc.get_message_info(self.account.id, self.id)
def get_webxdc_info(self) -> dict:
return self._rpc.get_webxdc_info(self.account.id, self.id)

View File

@@ -4,7 +4,6 @@ import os
import random
from typing import AsyncGenerator, Optional
import py
import pytest
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
@@ -12,6 +11,14 @@ from ._utils import futuremethod
from .rpc import Rpc
def get_temp_credentials() -> dict:
domain = os.getenv("CHATMAIL_DOMAIN")
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
password = f"{username}${username}"
addr = f"{username}@{domain}"
return {"email": addr, "password": password}
class ACFactory:
def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat
@@ -24,25 +31,26 @@ class ACFactory:
def get_unconfigured_bot(self) -> Bot:
return Bot(self.get_unconfigured_account())
def get_credentials(self) -> (str, str):
domain = os.getenv("CHATMAIL_DOMAIN")
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
return f"{username}@{domain}", f"{username}${username}"
def new_preconfigured_account(self) -> Account:
"""Make a new account with configuration options set, but configuration not started."""
credentials = get_temp_credentials()
account = self.get_unconfigured_account()
account.set_config("addr", credentials["email"])
account.set_config("mail_pw", credentials["password"])
assert not account.is_configured()
return account
@futuremethod
def new_configured_account(self):
addr, password = self.get_credentials()
account = self.get_unconfigured_account()
params = {"addr": addr, "password": password}
yield account.add_transport.future(params)
account = self.new_preconfigured_account()
yield account.configure.future()
assert account.is_configured()
return account
def new_configured_bot(self) -> Bot:
addr, password = self.get_credentials()
credentials = get_temp_credentials()
bot = self.get_unconfigured_bot()
bot.configure(addr, password)
bot.configure(credentials["email"], credentials["password"])
return bot
@futuremethod
@@ -79,7 +87,7 @@ class ACFactory:
) -> Message:
if not from_account:
from_account = (self.get_online_accounts(1))[0]
to_contact = from_account.create_contact(to_account)
to_contact = from_account.create_contact(to_account.get_config("addr"))
if group:
to_chat = from_account.create_group(group)
to_chat.add_contact(to_contact)
@@ -116,50 +124,3 @@ def rpc(tmp_path) -> AsyncGenerator:
@pytest.fixture
def acfactory(rpc) -> AsyncGenerator:
return ACFactory(DeltaChat(rpc))
@pytest.fixture
def data():
"""Test data."""
class Data:
def __init__(self) -> None:
for path in reversed(py.path.local(__file__).parts()):
datadir = path.join("test-data")
if datadir.isdir():
self.path = datadir
return
raise Exception("Data path cannot be found")
def get_path(self, bn):
"""return path of file or None if it doesn't exist."""
fn = os.path.join(self.path, *bn.split("/"))
assert os.path.exists(fn)
return fn
def read_path(self, bn, mode="r"):
fn = self.get_path(bn)
if fn is not None:
with open(fn, mode) as f:
return f.read()
return None
return Data()
@pytest.fixture
def log():
"""Log printer fixture."""
class Printer:
def section(self, msg: str) -> None:
print()
print("=" * 10, msg, "=" * 10)
def step(self, msg: str) -> None:
print("-" * 5, "step " + msg, "-" * 5)
def indent(self, msg: str) -> None:
print(" " + msg)
return Printer()

View File

@@ -13,11 +13,10 @@ def test_event_on_configuration(acfactory: ACFactory) -> None:
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
"""
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account = acfactory.new_preconfigured_account()
account.clear_all_events()
assert not account.is_configured()
future = account.add_transport.future({"addr": addr, "password": password})
future = account.configure.future()
while True:
event = account.wait_for_event()
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:

View File

@@ -48,7 +48,8 @@ def test_delivery_status(acfactory: ACFactory) -> None:
"""
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice.clear_all_events()
@@ -118,7 +119,8 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
"""
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("hi")
@@ -148,7 +150,8 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("hi")

View File

@@ -175,11 +175,17 @@ def test_no_duplicate_messages(acfactory, path_to_webxdc):
threading.Thread(target=thread_run, daemon=True).start()
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
n = int(bytes(event.data).decode())
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
n = int(bytes(event.data).decode())
break
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
assert int(bytes(event.data).decode()) > n
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert int(bytes(event.data).decode()) > n
break
def test_no_reordering(acfactory, path_to_webxdc):
@@ -223,5 +229,8 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
ac2_hello_msg_snapshot.chat.accept()
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
assert event.msg_id == ac1_webxdc_msg.id
while 1:
event = ac1.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED:
assert event.msg_id == ac1_webxdc_msg.id
break

View File

@@ -21,7 +21,6 @@ def test_autocrypt_setup_message_key_transfer(acfactory):
alice2.set_config("addr", alice1.get_config("addr"))
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
alice2.configure()
alice2.bring_online()
setup_code = alice1.initiate_autocrypt_key_transfer()
msg = wait_for_autocrypt_setup_message(alice2)
@@ -35,12 +34,10 @@ def test_autocrypt_setup_message_key_transfer(acfactory):
def test_ac_setup_message_twice(acfactory):
alice1 = acfactory.get_online_account()
alice2 = acfactory.get_unconfigured_account()
alice2.set_config("addr", alice1.get_config("addr"))
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
alice2.configure()
alice2.bring_online()
# Send the first Autocrypt Setup Message and ignore it.
_setup_code = alice1.initiate_autocrypt_key_transfer()

View File

@@ -1,110 +0,0 @@
from imap_tools import AND
from deltachat_rpc_client import EventType
from deltachat_rpc_client.const import MessageState
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
log.section("send out message without bcc to ourselves")
ac1.set_config("bcc_self", "0")
chat = ac1.create_chat(ac2)
self_addr = ac1.get_config("addr")
other_addr = ac2.get_config("addr")
msg_out = chat.send_text("message1")
assert not msg_out.get_snapshot().is_forwarded
# wait for send out (no BCC)
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
assert ac1.get_config("bcc_self") == "0"
assert self_addr not in ev.msg
assert other_addr in ev.msg
log.section("ac1: setting bcc_self=1")
ac1.set_config("bcc_self", "1")
log.section("send out message with bcc to ourselves")
msg_out = chat.send_text("message2")
# wait for send out (BCC)
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
assert ac1.get_config("bcc_self") == "1"
# Second client receives only second message, but not the first.
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
# now make sure we are sending message to ourselves too
assert self_addr in ev.msg
assert self_addr in ev.msg
# BCC-self messages are marked as seen by the sender device.
while True:
event = ac1.wait_for_event()
if event.kind == EventType.INFO and event.msg.endswith("Marked messages 1 in folder INBOX as seen."):
break
# Check that the message is marked as seen on IMAP.
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.connect()
ac1_direct_imap.select_folder("Inbox")
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_multidevice_sync_seen(acfactory, log):
"""Test that message marked as seen on one device is marked as seen on another."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
ac1.set_config("bcc_self", "1")
ac1_clone.set_config("bcc_self", "1")
ac1_chat = ac1.create_chat(ac2)
ac1_clone_chat = ac1_clone.create_chat(ac2)
ac2_chat = ac2.create_chat(ac1)
log.section("Send a message from ac2 to ac1 and check that it's 'fresh'")
ac2_chat.send_text("Hi")
ac1_message = ac1.wait_for_incoming_msg()
ac1_clone_message = ac1_clone.wait_for_incoming_msg()
assert ac1_chat.get_fresh_message_count() == 1
assert ac1_clone_chat.get_fresh_message_count() == 1
assert ac1_message.get_snapshot().state == MessageState.IN_FRESH
assert ac1_clone_message.get_snapshot().state == MessageState.IN_FRESH
log.section("ac1 marks message as seen on the first device")
ac1.mark_seen_messages([ac1_message])
assert ac1_message.get_snapshot().state == MessageState.IN_SEEN
log.section("ac1 clone detects that message is marked as seen")
ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
assert ev.chat_id == ac1_clone_chat.id
log.section("Send an ephemeral message from ac2 to ac1")
ac2_chat.set_ephemeral_timer(60)
ac1.wait_for_event(EventType.CHAT_EPHEMERAL_TIMER_MODIFIED)
ac1.wait_for_incoming_msg()
ac1_clone.wait_for_event(EventType.CHAT_EPHEMERAL_TIMER_MODIFIED)
ac1_clone.wait_for_incoming_msg()
ac2_chat.send_text("Foobar")
ac1_message = ac1.wait_for_incoming_msg()
ac1_clone_message = ac1_clone.wait_for_incoming_msg()
assert "Ephemeral timer: 60\n" in ac1_message.get_info()
assert "Expires: " not in ac1_clone_message.get_info()
assert "Ephemeral timer: 60\n" in ac1_message.get_info()
assert "Expires: " not in ac1_clone_message.get_info()
ac1_message.mark_seen()
assert "Expires: " in ac1_message.get_info()
ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
assert ev.chat_id == ac1_clone_chat.id
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN
# Test that the timer is started on the second device after synchronizing the seen status.
assert "Expires: " in ac1_clone_message.get_info()

View File

@@ -76,11 +76,17 @@ def test_qr_securejoin(acfactory, protect):
bob.secure_join(qr_code)
# Alice deletes "vg-request".
alice.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
while True:
event = alice.wait_for_event()
if event["kind"] == "ImapMessageDeleted":
break
alice.wait_for_securejoin_inviter_success()
# Bob deletes "vg-auth-required", Alice deletes "vg-request-with-auth".
for ac in [alice, bob]:
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
while True:
event = ac.wait_for_event()
if event["kind"] == "ImapMessageDeleted":
break
bob.wait_for_securejoin_joiner_success()
# Test that Alice verified Bob's profile.
@@ -89,7 +95,7 @@ def test_qr_securejoin(acfactory, protect):
assert alice_contact_bob_snapshot.is_verified
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected == protect
# Test that Bob verified Alice's profile.
@@ -117,7 +123,8 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -154,8 +161,11 @@ def test_qr_readreceipt(acfactory) -> None:
logging.info("Alice creates a verified group")
group = alice.create_group("Group", protect=True)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
bob_addr = bob.get_config("addr")
charlie_addr = charlie.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_charlie = alice.create_contact(charlie_addr, "Charlie")
group.add_contact(alice_contact_bob)
group.add_contact(alice_contact_charlie)
@@ -182,7 +192,7 @@ def test_qr_readreceipt(acfactory) -> None:
charlie_snapshot = charlie_message.get_snapshot()
assert charlie_snapshot.text == "Hi from Bob!"
bob_contact_charlie = bob.create_contact(charlie, "Charlie")
bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie")
assert not bob.get_chat_by_contact(bob_contact_charlie)
logging.info("Charlie reads Bob's message")
@@ -453,12 +463,12 @@ def test_qr_new_group_unblocked(acfactory):
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
@pytest.mark.skip(reason="AEAP is disabled for now")
def test_aeap_flow_verified(acfactory):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2 = acfactory.get_online_accounts(2)
addr, password = acfactory.get_credentials()
# ac1new is only used to get a new address.
ac1new = acfactory.new_preconfigured_account()
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
@@ -478,8 +488,8 @@ def test_aeap_flow_verified(acfactory):
assert msg_in_1.text == msg_out.text
logging.info("changing email account")
ac1.set_config("addr", addr)
ac1.set_config("mail_pw", password)
ac1.set_config("addr", ac1new.get_config("addr"))
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
ac1.stop_io()
ac1.configure()
ac1.start_io()
@@ -492,9 +502,11 @@ def test_aeap_flow_verified(acfactory):
msg_in_2_snapshot = msg_in_2.get_snapshot()
assert msg_in_2_snapshot.text == msg_out.text
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
assert msg_in_2.get_sender_contact().get_snapshot().address == addr
assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
assert ac1new.get_config("addr") in [
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
]
def test_gossip_verification(acfactory) -> None:
@@ -510,9 +522,9 @@ def test_gossip_verification(acfactory) -> None:
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
bob_contact_alice = bob.create_contact(alice, "Alice")
bob_contact_carol = bob.create_contact(carol, "Carol")
carol_contact_alice = carol.create_contact(alice, "Alice")
bob_contact_alice = bob.create_contact(alice.get_config("addr"), "Alice")
bob_contact_carol = bob.create_contact(carol.get_config("addr"), "Carol")
carol_contact_alice = carol.create_contact(alice.get_config("addr"), "Alice")
logging.info("Bob creates an Autocrypt group")
bob_group_chat = bob.create_group("Autocrypt Group")
@@ -563,7 +575,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 waits for member added message and creates a QR code.
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
assert snapshot.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
ac1_qr_code = snapshot.chat.get_qr_code()
# ac2 verifies ac1
@@ -572,7 +584,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
ac2.wait_for_securejoin_joiner_success()
# ac1 is verified for ac2.
ac2_contact_ac1 = ac2.create_contact(ac1, "")
ac2_contact_ac1 = ac2.create_contact(ac1.get_config("addr"), "")
assert ac2_contact_ac1.get_snapshot().is_verified
# ac1 resetups the account.
@@ -587,7 +599,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# header sent by old ac1.
while True:
# ac1 sends a message to ac2.
ac1_contact_ac2 = ac1.create_contact(ac2, "")
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
ac1_chat_ac2.send_text("Hello!")
@@ -646,7 +658,7 @@ def test_withdraw_securejoin_qr(acfactory):
alice.clear_all_events()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
bob_chat.leave()

View File

@@ -61,79 +61,61 @@ def test_acfactory(acfactory) -> None:
def test_configure_starttls(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_transport(
{
"addr": addr,
"password": password,
"imapSecurity": "starttls",
"smtpSecurity": "starttls",
},
)
account = acfactory.new_preconfigured_account()
# Use STARTTLS
account.set_config("mail_security", "2")
account.set_config("send_security", "2")
account.configure()
assert account.is_configured()
def test_configure_ip(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
account = acfactory.new_preconfigured_account()
domain = account.get_config("addr").rsplit("@")[-1]
ip_address = socket.gethostbyname(domain)
# This should fail TLS check.
account.set_config("mail_server", ip_address)
with pytest.raises(JsonRpcError):
account.add_transport(
{
"addr": addr,
"password": password,
# This should fail TLS check.
"imapServer": ip_address,
},
)
account.configure()
def test_configure_alternative_port(acfactory) -> None:
"""Test that configuration with alternative port 443 works."""
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_transport(
{
"addr": addr,
"password": password,
"imapPort": 443,
"smtpPort": 443,
},
)
assert account.is_configured()
account = acfactory.new_preconfigured_account()
account.set_config("mail_port", "443")
account.set_config("send_port", "443")
account.configure()
def test_list_transports(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_transport(
{
"addr": addr,
"password": password,
"imapUser": addr,
},
)
transports = account._rpc.list_transports(account.id)
assert len(transports) == 1
params = transports[0]
assert params["addr"] == addr
assert params["password"] == password
assert params["imapUser"] == addr
def test_configure_username(acfactory) -> None:
account = acfactory.new_preconfigured_account()
addr = account.get_config("addr")
account.set_config("mail_user", addr)
account.configure()
assert account.get_config("configured_mail_user") == addr
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
event = bob.wait_for_incoming_msg_event()
chat_id = event.chat_id
msg_id = event.msg_id
while True:
event = bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
@@ -192,7 +174,8 @@ def test_account(acfactory) -> None:
def test_chat(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -258,7 +241,7 @@ def test_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
assert repr(alice_contact_bob)
@@ -275,7 +258,8 @@ def test_contact(acfactory) -> None:
def test_message(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -303,37 +287,13 @@ def test_message(acfactory) -> None:
assert reactions == snapshot.reactions
def test_selfavatar_sync(acfactory, data, log) -> None:
alice = acfactory.get_online_account()
log.section("Alice adds a second device")
alice2 = alice.clone()
log.section("Second device goes online")
alice2.start_io()
log.section("First device changes avatar")
image = data.get_path("image/avatar1000x1000.jpg")
alice.set_config("selfavatar", image)
avatar_config = alice.get_config("selfavatar")
avatar_hash = os.path.basename(avatar_config)
print("Info: avatar hash is ", avatar_hash)
log.section("First device receives avatar change")
alice2.wait_for_event(EventType.SELFAVATAR_CHANGED)
avatar_config2 = alice2.get_config("selfavatar")
avatar_hash2 = os.path.basename(avatar_config2)
print("Info: avatar hash on second device is ", avatar_hash2)
assert avatar_hash == avatar_hash2
assert avatar_config != avatar_config2
def test_reaction_seen_on_another_dev(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice2 = alice.clone()
alice2.start_io()
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -345,12 +305,20 @@ def test_reaction_seen_on_another_dev(acfactory) -> None:
snapshot.chat.accept()
message.send_reaction("😎")
for a in [alice, alice2]:
a.wait_for_event(EventType.INCOMING_REACTION)
while True:
event = a.wait_for_event()
if event.kind == EventType.INCOMING_REACTION:
break
alice2.clear_all_events()
alice_chat_bob.mark_noticed()
chat_id = alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id
alice2_chat_bob = alice2.create_chat(bob)
while True:
event = alice2.wait_for_event()
if event.kind == EventType.MSGS_NOTICED:
chat_id = event.chat_id
break
alice2_contact_bob = alice2.get_contact_by_addr(bob_addr)
alice2_chat_bob = alice2_contact_bob.create_chat()
assert chat_id == alice2_chat_bob.id
@@ -358,19 +326,24 @@ def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
# Alice becomes a bot.
alice.set_config("bot", "1")
alice_chat_bob.send_text("Hello!")
event = bob.wait_for_incoming_msg_event()
message = bob.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
assert snapshot.chat_id == event.chat_id
assert snapshot.text == "Hello!"
assert snapshot.is_bot
while True:
event = bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.chat_id == event.chat_id
assert snapshot.text == "Hello!"
assert snapshot.is_bot
break
def test_bot(acfactory) -> None:
@@ -417,11 +390,9 @@ def test_wait_next_messages(acfactory) -> None:
alice = acfactory.new_configured_account()
# Create a bot account so it does not receive device messages in the beginning.
addr, password = acfactory.get_credentials()
bot = acfactory.get_unconfigured_account()
bot = acfactory.new_preconfigured_account()
bot.set_config("bot", "1")
bot.add_transport({"addr": addr, "password": password})
assert bot.is_configured()
bot.configure()
# There are no old messages and the call returns immediately.
assert not bot.wait_next_messages()
@@ -430,7 +401,8 @@ def test_wait_next_messages(acfactory) -> None:
# Bot starts waiting for messages.
next_messages_task = executor.submit(bot.wait_next_messages)
alice_contact_bot = alice.create_contact(bot, "Bot")
bot_addr = bot.get_config("addr")
alice_contact_bot = alice.create_contact(bot_addr, "Bot")
alice_chat_bot = alice_contact_bot.create_chat()
alice_chat_bot.send_text("Hello!")
@@ -454,7 +426,9 @@ def test_import_export_backup(acfactory, tmp_path) -> None:
def test_import_export_keys(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello Bob!")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
@@ -504,7 +478,9 @@ def test_provider_info(rpc) -> None:
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
# Bob creates chat manually so chat with Alice is accepted.
alice_chat_bob = alice_contact_bob.create_chat()
@@ -528,7 +504,10 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
# Alice reads Bob's message.
message.mark_seen()
bob.wait_for_event(EventType.MSG_READ)
while True:
event = bob.wait_for_event()
if event.kind == EventType.MSG_READ:
break
# Bob sends a message to Alice, it should also be encrypted.
bob_chat_alice.send_text("Hi Alice!")
@@ -600,13 +579,9 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account()
ac2.add_transport({"addr": addr, "password": password})
ac2 = acfactory.new_preconfigured_account()
ac2.configure()
ac2.set_config("mvbox_move", "1")
assert ac2.is_configured()
ac2.bring_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
@@ -650,7 +625,9 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
chat.send_text("Hello Alice!")
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
contact = alice.create_contact(account)
contact_addr = account.get_config("addr")
contact = alice.create_contact(contact_addr, "")
alice_group.add_contact(contact)
if n_accounts == 2:
@@ -700,7 +677,10 @@ def test_markseen_contact_request(acfactory):
assert message2.get_snapshot().state == MessageState.IN_FRESH
message.mark_seen()
bob2.wait_for_event(EventType.MSGS_NOTICED)
while True:
event = bob2.wait_for_event()
if event.kind == EventType.MSGS_NOTICED:
break
assert message2.get_snapshot().state == MessageState.IN_SEEN
@@ -748,7 +728,10 @@ def test_no_old_msg_is_fresh(acfactory):
assert ac1.create_chat(ac2).get_fresh_message_count() == 1
assert len(list(ac1.get_fresh_messages())) == 1
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
while True:
event = ac1.wait_for_event()
if event.kind == EventType.IMAP_INBOX_IDLE:
break
logging.info("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
ac1_clone_chat.send_text("Hi back")
@@ -757,71 +740,3 @@ def test_no_old_msg_is_fresh(acfactory):
assert ev.chat_id == first_msg.get_snapshot().chat_id
assert ac1.create_chat(ac2).get_fresh_message_count() == 0
assert len(list(ac1.get_fresh_messages())) == 0
def test_rename_synchronization(acfactory):
"""Test synchronization of contact renaming."""
alice, bob = acfactory.get_online_accounts(2)
alice2 = alice.clone()
alice2.bring_online()
bob.set_config("displayname", "Bob")
bob.create_chat(alice).send_text("Hello!")
alice_msg = alice.wait_for_incoming_msg().get_snapshot()
alice2_msg = alice2.wait_for_incoming_msg().get_snapshot()
assert alice2_msg.sender.get_snapshot().display_name == "Bob"
alice_msg.sender.set_name("Bobby")
alice2.wait_for_event(EventType.CONTACTS_CHANGED)
assert alice2_msg.sender.get_snapshot().display_name == "Bobby"
def test_rename_group(acfactory):
"""Test renaming the group."""
alice, bob = acfactory.get_online_accounts(2)
alice_group = alice.create_group("Test group")
alice_contact_bob = alice.create_contact(bob)
alice_group.add_contact(alice_contact_bob)
alice_group.send_text("Hello!")
bob_msg = bob.wait_for_incoming_msg()
bob_chat = bob_msg.get_snapshot().chat
assert bob_chat.get_basic_snapshot().name == "Test group"
for name in ["Baz", "Foo bar", "Xyzzy"]:
alice_group.set_name(name)
bob.wait_for_incoming_msg_event()
assert bob_chat.get_basic_snapshot().name == name
def test_get_all_accounts_deadlock(rpc):
"""Regression test for get_all_accounts deadlock."""
for _ in range(100):
all_accounts = rpc.get_all_accounts.future()
rpc.add_account()
all_accounts()
def test_delete_deltachat_folder(acfactory, direct_imap):
"""Test that DeltaChat folder is recreated if user deletes it manually."""
ac1 = acfactory.new_configured_account()
ac1.set_config("mvbox_move", "1")
ac1.bring_online()
ac1_direct_imap = direct_imap(ac1)
ac1_direct_imap.conn.folder.delete("DeltaChat")
assert "DeltaChat" not in ac1_direct_imap.list_folders()
# Wait until new folder is created and UIDVALIDITY is updated.
while True:
event = ac1.wait_for_event()
if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg:
break
ac2 = acfactory.get_online_account()
ac2.create_chat(ac1).send_text("hello")
msg = ac1.wait_for_incoming_msg().get_snapshot()
assert msg.text == "hello"
assert "DeltaChat" in ac1_direct_imap.list_folders()

View File

@@ -1,7 +1,8 @@
def test_vcard(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
alice_chat_bob = alice_contact_bob.create_chat()

View File

@@ -1,13 +1,20 @@
from deltachat_rpc_client import EventType
def test_webxdc(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
event = bob.wait_for_incoming_msg_event()
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
message = bob.get_message_by_id(event.msg_id)
while True:
event = bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
message = bob.get_message_by_id(event.msg_id)
break
webxdc_info = message.get_webxdc_info()
assert webxdc_info == {
@@ -44,7 +51,8 @@ def test_webxdc(acfactory) -> None:
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")

View File

@@ -12,8 +12,11 @@ setenv =
RUST_MIN_STACK=8388608
passenv =
CHATMAIL_DOMAIN
dependency_groups =
dev
deps =
pytest
pytest-timeout
pytest-xdist
imap-tools
[testenv:lint]
skipsdist = True
@@ -21,7 +24,7 @@ skip_install = True
deps =
ruff
commands =
ruff format --diff src/ examples/ tests/
ruff format --quiet --diff src/ examples/ tests/
ruff check src/ examples/ tests/
[pytest]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.159.1"
version = "1.157.2"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "1.159.1"
"version": "1.157.2"
}

View File

@@ -30,7 +30,7 @@ async fn main() {
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
// until the user presses enter."
if let Err(error) = &r {
log::error!("Error: {error:#}.")
log::error!("Fatal error: {error:#}.")
}
std::process::exit(if r.is_ok() { 0 } else { 1 });
}

View File

@@ -24,6 +24,8 @@ ignore = [
# Please keep this list alphabetically sorted.
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "base64", version = "<0.21" },
{ name = "base64", version = "0.21.7" },
{ name = "bitflags", version = "1.3.2" },
{ name = "core-foundation", version = "0.9.4" },
{ name = "event-listener", version = "2.5.3" },
@@ -31,11 +33,11 @@ skip = [
{ name = "getrandom", version = "0.2.12" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "linux-raw-sys", version = "0.4.14" },
{ name = "loom", version = "0.5.6" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "nix", version = "0.26.4" },
{ name = "nix", version = "0.27.1" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "0.3.1" },
{ name = "rand_core", version = "0.6.4" },
{ name = "rand", version = "0.8.5" },
@@ -43,13 +45,13 @@ skip = [
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rtnetlink", version = "0.13.1" },
{ name = "rustix", version = "0.38.44" },
{ name = "security-framework", version = "2.11.1" },
{ name = "strum_macros", version = "0.26.2" },
{ name = "strum", version = "0.26.2" },
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "unicode-width", version = "0.1.11" },
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },

View File

@@ -584,9 +584,6 @@
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
(python3.withPackages (pypkgs: with pypkgs; [
tox
]))
];
};
}

View File

@@ -25,8 +25,8 @@ def test_echo_quit_plugin(acfactory, lp):
(ac1,) = acfactory.get_online_accounts(1)
lp.sec("sending a message to the bot")
bot_chat = ac1.qr_setup_contact(botproc.qr)
ac1._evtracker.wait_securejoin_joiner_progress(1000)
bot_contact = ac1.create_contact(botproc.addr)
bot_chat = bot_contact.create_chat()
bot_chat.send_text("hello")
lp.sec("waiting for the reply message from the bot to arrive")
@@ -48,9 +48,7 @@ def test_group_tracking_plugin(acfactory, lp):
ac2.add_account_plugin(FFIEventLogger(ac2))
lp.sec("creating bot test group with bot")
bot_chat = ac1.qr_setup_contact(botproc.qr)
ac1._evtracker.wait_securejoin_joiner_progress(1000)
bot_contact = bot_chat.get_contacts()[0]
bot_contact = ac1.create_contact(botproc.addr)
ch = ac1.create_group_chat("bot test group")
ch.add_contact(bot_contact)
ch.send_text("hello")
@@ -62,7 +60,7 @@ def test_group_tracking_plugin(acfactory, lp):
)
lp.sec("adding third member {}".format(ac2.get_config("addr")))
contact3 = ac1.create_contact(ac2)
contact3 = ac1.create_contact(ac2.get_config("addr"))
ch.add_contact(contact3)
reply = ac1._evtracker.wait_next_incoming_message()

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.159.1"
version = "1.157.2"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"

View File

@@ -55,8 +55,6 @@ def run_cmdline(argv=None, account_plugins=None):
args = parser.parse_args(argv[1:])
ac = Account(args.db)
qr = ac.get_setup_contact_qr()
print(qr)
ac.run_account(addr=args.email, password=args.password, account_plugins=account_plugins, show_ffi=args.show_ffi)

View File

@@ -280,12 +280,6 @@ class Account:
:param name: (optional) display name for this contact
:returns: :class:`deltachat.contact.Contact` instance.
"""
if isinstance(obj, Account):
if not obj.is_configured():
raise ValueError("Can only add configured accounts as contacts")
assert name is None
vcard = obj.get_self_contact().make_vcard()
return self.import_vcard(vcard)[0]
(name, addr) = self.get_contact_addr_and_name(obj, name)
name_c = as_dc_charpointer(name)
addr_c = as_dc_charpointer(addr)
@@ -374,11 +368,6 @@ class Account:
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query_c), lib.dc_array_unref)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def import_vcard(self, vcard):
"""Import a vCard and return an array of contacts."""
dc_array = ffi.gc(lib.dc_import_vcard(self._dc_context, as_dc_charpointer(vcard)), lib.dc_array_unref)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_fresh_messages(self) -> Generator[Message, None, None]:
"""yield all fresh messages from all chats."""
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)

View File

@@ -90,14 +90,6 @@ class Contact:
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
return from_optional_dc_charpointer(dc_res)
def make_vcard(self) -> str:
"""Make a contact vCard.
:returns: vCard
"""
dc_context = self.account._dc_context
return from_dc_charpointer(lib.dc_make_vcard(dc_context, self.id))
@property
def status(self):
"""Get contact status.

View File

@@ -649,9 +649,6 @@ class BotProcess:
def __init__(self, popen, addr) -> None:
self.popen = popen
# The first thing the bot prints to stdout is an invite link.
self.qr = self.popen.stdout.readline()
self.addr = addr
# we read stdout as quickly as we can in a thread and make

View File

@@ -1,6 +1,7 @@
import sys
import time
import pytest
import deltachat as dc
@@ -16,6 +17,8 @@ class TestGroupStressTests:
lp.sec("ac1: send message to new group chat")
msg1 = chat.send_text("hello")
assert msg1.is_encrypted()
gossiped_timestamp = chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp > 0
assert chat.num_contacts() == 3 + 1
@@ -44,13 +47,19 @@ class TestGroupStressTests:
assert to_remove.addr in sysmsg.text
assert sysmsg.chat.num_contacts() == 3
# Receiving message about removed contact does not reset gossip
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
lp.sec("ac1: sending another message to the chat")
chat.send_text("hello2")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello2"
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
lp.sec("ac1: adding fifth member to the chat")
chat.add_contact(ac5)
# Adding contact to chat resets gossiped_timestamp
assert chat.get_summary()["gossiped_timestamp"] >= gossiped_timestamp
lp.sec("ac2: receiving system message about contact addition")
sysmsg = ac2._evtracker.wait_next_incoming_message()
@@ -187,6 +196,118 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert msg.is_encrypted()
@pytest.mark.parametrize("mvbox_move", [False, True])
def test_fetch_existing(acfactory, lp, mvbox_move):
"""Delta Chat reads the recipients from old emails sent by the user and adds them as contacts.
This way, we can already offer them some email addresses they can write to.
Also, the newest existing emails from each folder are fetched during onboarding.
Additionally tests that bcc_self messages moved to the mvbox/sentbox are marked as read."""
def assert_folders_configured(ac):
"""There was a bug that scan_folders() set the configured folders to None under some circumstances.
So, check that they are still configured:"""
assert ac.get_config("configured_sentbox_folder") == "Sent"
if mvbox_move:
assert ac.get_config("configured_mvbox_folder")
ac1 = acfactory.new_online_configuring_account(mvbox_move=mvbox_move)
ac2 = acfactory.new_online_configuring_account()
acfactory.wait_configured(ac1)
ac1.direct_imap.create_folder("Sent")
ac1.set_config("sentbox_watch", "1")
# We need to reconfigure to find the new "Sent" folder.
# `scan_folders()`, which runs automatically shortly after `start_io()` is invoked,
# would also find the "Sent" folder, but it would be too late:
# The sentbox thread, started by `start_io()`, would have seen that there is no
# ConfiguredSentboxFolder and do nothing.
acfactory._acsetup.start_configure(ac1)
acfactory.bring_accounts_online()
assert_folders_configured(ac1)
lp.sec("send out message with bcc to ourselves")
ac1.set_config("bcc_self", "1")
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message text")
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
if mvbox_move:
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
else:
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
assert_folders_configured(ac1)
lp.sec("create a cloned ac1 and fetch contact history during configure")
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
ac1_clone.set_config("fetch_existing_msgs", "1")
acfactory.wait_configured(ac1_clone)
ac1_clone.start_io()
assert_folders_configured(ac1_clone)
lp.sec("check that ac2 contact was fetched during configure")
ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
ac2_addr = ac2.get_config("addr")
assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())
assert_folders_configured(ac1_clone)
lp.sec("check that messages changed events arrive for the correct message")
msg = ac1_clone._evtracker.wait_next_messages_changed()
assert msg.text == "message text"
assert_folders_configured(ac1)
assert_folders_configured(ac1_clone)
def test_fetch_existing_msgs_group_and_single(acfactory, lp):
"""There was a bug concerning fetch-existing-msgs:
A sent a message to you, adding you to a group. This created a contact request.
You wrote a message to A, creating a chat.
...but the group stayed blocked.
So, after fetch-existing-msgs you have one contact request and one chat with the same person.
See https://github.com/deltachat/deltachat-core-rust/issues/2097"""
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account()
acfactory.bring_accounts_online()
lp.sec("receive a message")
ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message")
ac1._evtracker.wait_next_incoming_message()
lp.sec("send out message with bcc to ourselves")
ac1.set_config("bcc_self", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
# wait until the bcc_self message arrives
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
lp.sec("Clone online account and let it fetch the existing messages")
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
ac1_clone.set_config("fetch_existing_msgs", "1")
acfactory.wait_configured(ac1_clone)
ac1_clone.start_io()
ac1_clone._evtracker.wait_idle_inbox_ready()
chats = ac1_clone.get_chats()
assert len(chats) == 4 # two newly created chats + self-chat + device-chat
group_chat = [c for c in chats if c.get_name() == "group name"][0]
assert group_chat.is_group()
(private_chat,) = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()]
assert not private_chat.is_group()
group_messages = group_chat.get_messages()
assert len(group_messages) == 1
assert group_messages[0].text == "incoming, unencrypted group message"
private_messages = private_chat.get_messages()
# We can't decrypt the message in this chat, so the chat is empty:
assert len(private_messages) == 0
def test_undecipherable_group(acfactory, lp):
"""Test how group messages that cannot be decrypted are
handled.
@@ -323,6 +444,63 @@ def test_ephemeral_timer(acfactory, lp):
assert chat1.get_ephemeral_timer() == 0
def test_multidevice_sync_seen(acfactory, lp):
"""Test that message marked as seen on one device is marked as seen on another."""
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account()
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
acfactory.bring_accounts_online()
ac1.set_config("bcc_self", "1")
ac1_clone.set_config("bcc_self", "1")
ac1_chat = ac1.create_chat(ac2)
ac1_clone_chat = ac1_clone.create_chat(ac2)
ac2_chat = ac2.create_chat(ac1)
lp.sec("Send a message from ac2 to ac1 and check that it's 'fresh'")
ac2_chat.send_text("Hi")
ac1_message = ac1._evtracker.wait_next_incoming_message()
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
assert ac1_chat.count_fresh_messages() == 1
assert ac1_clone_chat.count_fresh_messages() == 1
assert ac1_message.is_in_fresh
assert ac1_clone_message.is_in_fresh
lp.sec("ac1 marks message as seen on the first device")
ac1.mark_seen_messages([ac1_message])
assert ac1_message.is_in_seen
lp.sec("ac1 clone detects that message is marked as seen")
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert ev.data1 == ac1_clone_chat.id
assert ac1_clone_message.is_in_seen
lp.sec("Send an ephemeral message from ac2 to ac1")
ac2_chat.set_ephemeral_timer(60)
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
ac1._evtracker.wait_next_incoming_message()
ac1_clone._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
ac1_clone._evtracker.wait_next_incoming_message()
ac2_chat.send_text("Foobar")
ac1_message = ac1._evtracker.wait_next_incoming_message()
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
assert "Expires: " not in ac1_clone_message.get_message_info()
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
assert "Expires: " not in ac1_clone_message.get_message_info()
ac1.mark_seen_messages([ac1_message])
assert ac1_message.is_in_seen
assert "Expires: " in ac1_message.get_message_info()
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert ev.data1 == ac1_clone_chat.id
assert ac1_clone_message.is_in_seen
# Test that the timer is started on the second device after synchronizing the seen status.
assert "Expires: " in ac1_clone_message.get_message_info()
def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
"""The test for the bug #3836:
- Alice has two devices, the second is offline.

View File

@@ -54,6 +54,57 @@ def test_configure_unref(tmp_path):
lib.dc_context_unref(dc_context)
def test_one_account_send_bcc_setting(acfactory, lp):
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account()
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
acfactory.bring_accounts_online()
# test if sent messages are copied to it via BCC.
chat = acfactory.get_accepted_chat(ac1, ac2)
self_addr = ac1.get_config("addr")
other_addr = ac2.get_config("addr")
lp.sec("send out message without bcc to ourselves")
ac1.set_config("bcc_self", "0")
msg_out = chat.send_text("message1")
assert not msg_out.is_forwarded()
# wait for send out (no BCC)
ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "0"
# make sure we are not sending message to ourselves
assert self_addr not in ev.data2
assert other_addr in ev.data2
lp.sec("ac1: setting bcc_self=1")
ac1.set_config("bcc_self", "1")
lp.sec("send out message with bcc to ourselves")
msg_out = chat.send_text("message2")
# wait for send out (BCC)
ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "1"
# Second client receives only second message, but not the first.
ev_msg = ac1_clone._evtracker.wait_next_messages_changed()
assert ev_msg.text == msg_out.text
# now make sure we are sending message to ourselves too
assert self_addr in ev.data2
assert other_addr in ev.data2
# BCC-self messages are marked as seen by the sender device.
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
# Check that the message is marked as seen on IMAP.
ac1.direct_imap.select_folder("Inbox")
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -436,6 +487,26 @@ def test_forward_messages(acfactory, lp):
assert not chat3.get_messages()
def test_forward_encrypted_to_unencrypted(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
chat = acfactory.get_protected_chat(ac1, ac2)
lp.sec("ac1: send encrypted message to ac2")
txt = "This should be encrypted"
chat.send_text(txt)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == txt
assert msg.is_encrypted()
lp.sec("ac2: forward message to ac3 unencrypted")
unencrypted_chat = ac2.create_chat(ac3)
msg_id = msg.id
msg2 = unencrypted_chat.send_msg(msg)
assert msg2 == msg
assert msg.id != msg_id
assert not msg.is_encrypted()
def test_forward_own_message(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -798,6 +869,12 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
msg3.mark_seen()
assert not list(ac1.get_fresh_messages())
# Test that we do not gossip peer keys in 1-to-1 chat,
# as it makes no sense to gossip to peers their own keys.
# Gossip is only sent in encrypted messages,
# and we sent encrypted msg_back right above.
assert chat2b.get_summary()["gossiped_timestamp"] == 0
lp.sec("create group chat with two members, one of which has no encrypt state")
chat = ac1.create_group_chat("encryption test")
chat.add_contact(ac2)
@@ -807,6 +884,41 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
def test_gossip_optimization(acfactory, lp):
"""Test that gossip timestamp is updated when someone else sends gossip,
so we don't have to send gossip ourselves.
"""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
acfactory.introduce_each_other([ac1, ac2])
acfactory.introduce_each_other([ac2, ac3])
lp.sec("ac1 creates a group chat with ac2")
group_chat = ac1.create_group_chat("hello")
group_chat.add_contact(ac2)
msg = group_chat.send_text("hi")
# No Autocrypt gossip was sent yet.
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp == 0
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert msg.text == "hi"
lp.sec("ac2 adds ac3 to the group")
msg.chat.add_contact(ac3)
lp.sec("ac1 receives message from ac2 and updates gossip timestamp")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
# ac1 has updated the gossip timestamp even though no gossip was sent by ac1.
# ac1 does not need to send gossip because ac2 already did it.
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp == int(msg.time_sent.timestamp())
def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -819,7 +931,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
" wrapped using format=flowed and unwrapped on the receiver"
)
msg_out = chat.send_text(text1)
assert msg_out.is_encrypted()
assert not msg_out.is_encrypted()
lp.sec("wait for ac2 to receive multi-line non-unicode message")
msg_in = ac2._evtracker.wait_next_incoming_message()
@@ -828,7 +940,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
lp.sec("sending multi-line unicode text message from ac1 to ac2")
text2 = "äalis\nthis is ßßÄ"
msg_out = chat.send_text(text2)
assert msg_out.is_encrypted()
assert not msg_out.is_encrypted()
lp.sec("wait for ac2 to receive multi-line unicode message")
msg_in = ac2._evtracker.wait_next_incoming_message()
@@ -1234,7 +1346,7 @@ def test_qr_email_capitalization(acfactory, lp):
lp.sec("ac1 joins a verified group via a QR code")
ac1_chat = ac1.qr_join_chat(qr)
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr"))
assert msg.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
assert len(ac1_chat.get_contacts()) == 2
lp.sec("ac2 joins a verified group via a QR code")
@@ -1333,7 +1445,7 @@ def test_add_remove_member_remote_events(acfactory, lp):
lp.sec("ac1: add address2")
# note that if the above create_chat() would not
# happen we would not receive a proper member_added event
contact2 = chat.add_contact(ac3)
contact2 = chat.add_contact(ac3_addr)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get()
@@ -1773,7 +1885,9 @@ def test_name_changes(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("displayname", "Account 1")
chat12 = acfactory.get_accepted_chat(ac1, ac2)
# Similar to acfactory.get_accepted_chat, but without setting the contact name.
ac2.create_contact(ac1.get_config("addr")).create_chat()
chat12 = ac1.create_contact(ac2.get_config("addr")).create_chat()
contact = None
def update_name():
@@ -1938,6 +2052,23 @@ def test_scan_folders(acfactory, lp, folder, move, expected_destination):
assert len(ac1.direct_imap.get_all_messages()) == 0
def test_delete_deltachat_folder(acfactory):
"""Test that DeltaChat folder is recreated if user deletes it manually."""
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
ac2 = acfactory.new_online_configuring_account()
acfactory.wait_configured(ac1)
ac1.direct_imap.conn.folder.delete("DeltaChat")
assert "DeltaChat" not in ac1.direct_imap.list_folders()
acfactory.bring_accounts_online()
ac2.create_chat(ac1).send_text("hello")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert "DeltaChat" in ac1.direct_imap.list_folders()
def test_archived_muted_chat(acfactory, lp):
"""If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for
DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously.

View File

@@ -45,7 +45,7 @@ deps =
pygments
restructuredtext_lint
commands =
ruff format --diff setup.py src/deltachat examples/ tests/
ruff format --quiet --diff setup.py src/deltachat examples/ tests/
ruff check src/deltachat tests/ examples/
rst-lint --encoding 'utf-8' README.rst

View File

@@ -1 +1 @@
2025-04-12
2025-03-15

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.86.0
RUST_VERSION=1.84.1
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -215,7 +215,7 @@ if __name__ == "__main__":
" Config, ConfigDefault, Oauth2Authorizer, Provider, ProviderOptions, Server, Status,\n"
"};\n"
"use std::collections::HashMap;\n\n"
"use std::sync::LazyLock;\n\n"
"use once_cell::sync::Lazy;\n\n"
)
process_dir(Path(sys.argv[1]))
@@ -224,7 +224,7 @@ if __name__ == "__main__":
out_all += out_domains
out_all += "];\n\n"
out_all += "pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider>> = LazyLock::new(|| HashMap::from([\n"
out_all += "pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| HashMap::from([\n"
out_all += out_ids
out_all += "]));\n\n"
@@ -233,8 +233,8 @@ if __name__ == "__main__":
else:
now = datetime.datetime.fromisoformat(sys.argv[2])
out_all += (
"pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> = "
"LazyLock::new(|| chrono::NaiveDate::from_ymd_opt("
"pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "
"Lazy::new(|| chrono::NaiveDate::from_ymd_opt("
+ str(now.year)
+ ", "
+ str(now.month)

90
spec.md
View File

@@ -1,10 +1,10 @@
# Chatmail Specification
# chat-mail specification
Version: 0.36.0
Version: 0.35.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
This document roughly describes how chatmail
This document roughly describes how chat-mail
apps use the standard e-mail system
to implement typical messenger functions.
@@ -18,8 +18,6 @@ to implement typical messenger functions.
- [Add and remove members](#add-and-remove-members)
- [Change group name](#change-group-name)
- [Set group image](#set-group-image)
- [Request editing](#request-editing)
- [Request deletion](#request-deletion)
- [Set profile image](#set-profile-image)
- [Locations](#locations)
- [User locations](#user-locations)
@@ -306,84 +304,6 @@ To save data, it is RECOMMENDED
to add a `Chat-Group-Avatar` only on image changes.
# Request editing
To request recipients to edit the text of an already sent message,
the messenger MUST set the header `Chat-Edit`
with value set to the message-id of the message to edit
and the body to the new message text.
The body MAY be prefixed by a quote
and the emoji "✏️" directly before the new text.
Both MUST be skipped by the recipient.
Receiving messengers MUST look up the message-id from `Chat-Edit`,
replace the text and MAY indicate the edit in the UI.
The new message text MUST NOT be empty.
It is not possible to edit images or other attachments, including HTML messages.
However, they can be deleted for everyone.
Example:
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Message-ID: 00001@domain
Content-Type: text/plain
Hello wordl!
The typo from the message above can be fixed by the following message:
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Chat-Edit: 00001@domain
In-Reply-To: 00001@domain
Message-ID: 00002@domain
Content-Type: text/plain
On 2025-03-27, sender@domain wrote:
> Hello wordl!
Hello world!
# Request deletion
To request recipient to delete a message,
the messenger MUST set the header `Chat-Delete`
with the value set to the message-id of the message to delete.
Receiving messengers MUST look up the message-id, delete the corresponding message
and MAY indicating the deletion in the UI.
The sender MUST set the body to any, non-empty text.
The receiver MUST ignore the body.
Example:
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Message-ID: 00003@domain
Content-Type: text/plain
reminder for my pin: 1234
The message above can be requested for deletion by the following message:
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Chat-Delete: 00003@domain
Message-ID: 00004@domain
Content-Type: text/plain
foo
# Set profile image
A user MAY have a profile-image that MAY be distributed to their contacts.
@@ -455,7 +375,7 @@ eg. forwarded from a normal MUA.
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document addr="foo@domain">
<Document addr="ndh@deltachat.de">
<Placemark>
<Timestamp><when>2020-01-11T20:40:19Z</when></Timestamp>
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
@@ -622,4 +542,4 @@ We define the effective date of a message
as the sending time of the message as indicated by its Date header,
or the time of first receipt if that date is in the future or unavailable.
Copyright © Chatmail contributors.
Copyright © 2017-2021 Delta Chat contributors.

View File

@@ -4,7 +4,7 @@ use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{bail, ensure, Context as _, Result};
use anyhow::{ensure, Context as _, Result};
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
@@ -73,7 +73,9 @@ impl Accounts {
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists(), "{:?} does not exist", config_file);
let config = Config::from_file(config_file, writable).await?;
let config = Config::from_file(config_file, writable)
.await
.context("failed to load accounts config")?;
let events = Events::new();
let stockstrings = StockStrings::new();
let push_subscriber = PushSubscriber::new();
@@ -458,9 +460,7 @@ impl Config {
rx.await?;
Ok(())
});
if locked_rx.await.is_err() {
bail!("Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)");
};
locked_rx.await?;
Ok(Some(lock_task))
}

View File

@@ -4,12 +4,12 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fmt;
use std::sync::LazyLock;
use anyhow::Result;
use deltachat_contact_tools::EmailAddress;
use mailparse::MailHeaderMap;
use mailparse::ParsedMail;
use once_cell::sync::Lazy;
use crate::config::Config;
use crate::context::Context;
@@ -107,8 +107,7 @@ fn remove_comments(header: &str) -> Cow<'_, str> {
// In Pomsky, this is:
// "(" Codepoint* lazy ")"
// See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22
static RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
static RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
RE.replace_all(header, " ")
}

View File

@@ -174,7 +174,7 @@ async fn test_selfavatar_outside_blobdir() {
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
let avatar_path = Path::new(&avatar_blob);
assert!(
avatar_blob.ends_with("1e08d1c9398297c21dd3820f7db2324.jpg"),
avatar_blob.ends_with("009161310a6afc319163e4bcabd23b9.jpg"),
"The avatar filename should be its hash, put instead it's {avatar_blob}"
);
let scaled_avatar_size = file_size(avatar_path).await;

View File

@@ -130,7 +130,8 @@ pub(crate) enum CantSendReason {
/// Not a member of the chat.
NotAMember,
/// Temporary state for 1:1 chats while SecureJoin is in progress.
/// Temporary state for 1:1 chats while SecureJoin is in progress, after a timeout sending
/// messages (incl. unencrypted if we don't yet know the contact's pubkey) is allowed.
SecurejoinWait,
}
@@ -581,18 +582,7 @@ impl ChatId {
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
};
add_info_msg_with_cmd(
context,
self,
&text,
cmd,
timestamp_sort,
None,
None,
None,
None,
)
.await?;
add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?;
Ok(())
}
@@ -1090,8 +1080,7 @@ impl ChatId {
.unwrap_or(0))
}
/// Returns timestamp of the latest message in the chat,
/// including hidden messages or a draft if there is one.
/// Returns timestamp of the latest message in the chat.
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
let timestamp = context
.sql
@@ -1376,10 +1365,41 @@ impl ChatId {
}
pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> {
self.set_gossiped_timestamp(context, 0).await
}
/// Get timestamp of the last gossip sent in the chat.
/// Zero return value means that gossip was never sent.
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
let timestamp: Option<i64> = context
.sql
.query_get_value("SELECT gossiped_timestamp FROM chats WHERE id=?;", (self,))
.await?;
Ok(timestamp.unwrap_or_default())
}
pub(crate) async fn set_gossiped_timestamp(
self,
context: &Context,
timestamp: i64,
) -> Result<()> {
ensure!(
!self.is_special(),
"can not set gossiped timestamp for special chats"
);
info!(
context,
"Set gossiped_timestamp for chat {} to {}.", self, timestamp,
);
context
.sql
.execute("DELETE FROM gossip_timestamp WHERE chat_id=?", (self,))
.execute(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
(timestamp, self),
)
.await?;
Ok(())
}
@@ -1695,13 +1715,13 @@ impl Chat {
return Ok(Some(reason));
}
let reason = SecurejoinWait;
if !skip_fn(&reason) {
let (can_write, _) = self
if !skip_fn(&reason)
&& self
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?;
if !can_write {
return Ok(Some(reason));
}
.await?
> 0
{
return Ok(Some(reason));
}
Ok(None)
}
@@ -1713,32 +1733,28 @@ impl Chat {
Ok(self.why_cant_send(context).await?.is_none())
}
/// Returns if the chat can be sent to
/// and the remaining timeout for the 1:1 chat in-progress SecureJoin.
/// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin.
///
/// If the timeout has expired, adds an info message with additional information;
/// the chat still cannot be sent to in this case. See also [`CantSendReason::SecurejoinWait`].
/// If the timeout has expired, notifies the user that sending messages is possible. See also
/// [`CantSendReason::SecurejoinWait`].
pub(crate) async fn check_securejoin_wait(
&self,
context: &Context,
timeout: u64,
) -> Result<(bool, u64)> {
) -> Result<u64> {
if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected {
return Ok((true, 0));
return Ok(0);
}
// chat is single and unprotected:
// get last info message of type SecurejoinWait or SecurejoinWaitTimeout
let (mut param_wait, mut param_timeout) = (Params::new(), Params::new());
param_wait.set_cmd(SystemMessage::SecurejoinWait);
param_timeout.set_cmd(SystemMessage::SecurejoinWaitTimeout);
let (param_wait, param_timeout) = (param_wait.to_string(), param_timeout.to_string());
let (mut param0, mut param1) = (Params::new(), Params::new());
param0.set_cmd(SystemMessage::SecurejoinWait);
param1.set_cmd(SystemMessage::SecurejoinWaitTimeout);
let (param0, param1) = (param0.to_string(), param1.to_string());
let Some((param, ts_sort, ts_start)) = context
.sql
.query_row_optional(
"SELECT param, timestamp, timestamp_sent FROM msgs WHERE id=\
(SELECT MAX(id) FROM msgs WHERE chat_id=? AND param IN (?, ?))",
(self.id, &param_wait, &param_timeout),
(self.id, &param0, &param1),
|row| {
let param: String = row.get(0)?;
let ts_sort: i64 = row.get(1)?;
@@ -1748,13 +1764,11 @@ impl Chat {
)
.await?
else {
return Ok((true, 0));
return Ok(0);
};
if param == param_timeout {
return Ok((false, 0));
if param == param1 {
return Ok(0);
}
let now = time();
// Don't await SecureJoin if the clock was set back.
if ts_start <= now {
@@ -1762,14 +1776,13 @@ impl Chat {
.saturating_add(timeout.try_into()?)
.saturating_sub(now);
if timeout > 0 {
return Ok((false, timeout as u64));
return Ok(timeout as u64);
}
}
add_info_msg_with_cmd(
context,
self.id,
&stock_str::securejoin_takes_longer(context).await,
&stock_str::securejoin_wait_timeout(context).await,
SystemMessage::SecurejoinWaitTimeout,
// Use the sort timestamp of the "please wait" message, this way the added message is
// never sorted below the protection message if the SecureJoin finishes in parallel.
@@ -1777,11 +1790,10 @@ impl Chat {
Some(now),
None,
None,
None,
)
.await?;
Ok((false, 0))
context.emit_event(EventType::ChatModified(self.id));
Ok(0)
}
/// Checks if the user is part of a chat
@@ -1886,6 +1898,7 @@ impl Chat {
name: self.name.clone(),
archived: self.visibility == ChatVisibility::Archived,
param: self.param.to_string(),
gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?,
is_sending_locations: self.is_sending_locations,
color: self.get_color(context).await?,
profile_image: self
@@ -1976,7 +1989,13 @@ impl Chat {
if let Some(member_list_timestamp) = self.param.get_i64(Param::MemberListTimestamp) {
Ok(member_list_timestamp)
} else {
Ok(self.id.created_timestamp(context).await?)
let creation_timestamp: i64 = context
.sql
.query_get_value("SELECT created_timestamp FROM chats WHERE id=?", (self.id,))
.await
.context("SQL error querying created_timestamp")?
.context("Chat not found")?;
Ok(creation_timestamp)
}
}
@@ -2008,8 +2027,6 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
let new_rfc724_mid = create_outgoing_rfc724_mid();
if self.typ == Chattype::Single {
if let Some(id) = context
.sql
@@ -2031,9 +2048,7 @@ impl Chat {
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
{
msg.param.set_int(Param::AttachGroupImage, 1);
self.param
.remove(Param::Unpromoted)
.set_i64(Param::GroupNameTimestamp, timestamp);
self.param.remove(Param::Unpromoted);
self.update_param(context).await?;
// TODO: Remove this compat code needed because Core <= v1.143:
// - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also
@@ -2106,7 +2121,7 @@ impl Chat {
if references_vec.is_empty() {
// As a fallback, use our Message-ID,
// same as in the case of top-level message.
new_references = new_rfc724_mid.clone();
new_references = msg.rfc724_mid.clone();
} else {
new_references = references_vec.join(" ");
}
@@ -2116,8 +2131,9 @@ impl Chat {
// This allows us to identify replies to our message even if
// email server such as Outlook changes `Message-ID:` header.
// MUAs usually keep the first Message-ID in `References:` header unchanged.
new_references = new_rfc724_mid.clone();
new_references = msg.rfc724_mid.clone();
}
info!(context, "new references: {new_references:?}");
// add independent location to database
if msg.param.exists(Param::SetLatitude) {
@@ -2184,7 +2200,6 @@ impl Chat {
msg.chat_id = self.id;
msg.from_id = ContactId::SELF;
msg.rfc724_mid = new_rfc724_mid;
msg.timestamp_sort = timestamp;
// add message to the database
@@ -2428,6 +2443,9 @@ pub struct ChatInfo {
/// This is the string-serialised version of `Params` currently.
pub param: String,
/// Last time this client sent autocrypt gossip headers to this chat.
pub gossiped_timestamp: i64,
/// Whether this chat is currently sending location-stream messages.
pub is_sending_locations: bool,
@@ -2584,7 +2602,7 @@ pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> {
for chat_id in chat_ids {
let chat = Chat::load_from_db(context, chat_id).await?;
let (_, timeout) = chat
let timeout = chat
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?;
if timeout > 0 {
@@ -2989,12 +3007,6 @@ async fn prepare_send_msg(
///
/// The caller has to interrupt SMTP loop or otherwise process new rows.
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
if msg.param.get_cmd() == SystemMessage::GroupNameChanged {
msg.chat_id
.update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort)
.await?;
}
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
let mimefactory = MimeFactory::from_msg(context, msg.clone()).await?;
let attach_selfavatar = mimefactory.attach_selfavatar;
@@ -3015,10 +3027,10 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
// disabled by default is fine.
//
// `from` must be the last addr, see `receive_imf_inner()` why.
recipients.retain(|x| x.to_lowercase() != lowercase_from);
if (context.get_config_bool(Config::BccSelf).await?
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage)
&& (context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty())
if context.get_config_bool(Config::BccSelf).await?
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
recipients.push(from);
}
@@ -3040,6 +3052,11 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
msg.state = MessageState::OutDelivered;
return Ok(Vec::new());
}
if msg.param.get_cmd() == SystemMessage::GroupNameChanged {
msg.chat_id
.update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort)
.await?;
}
let rendered_msg = match mimefactory.render(context).await {
Ok(res) => Ok(res),
@@ -3066,6 +3083,10 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
let now = smeared_time(context);
if rendered_msg.is_gossiped {
msg.chat_id.set_gossiped_timestamp(context, now).await?;
}
if rendered_msg.last_added_location_id.is_some() {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
@@ -3880,9 +3901,7 @@ pub(crate) async fn add_contact_to_chat_ex(
let sync_qr_code_tokens;
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
chat.param
.remove(Param::Unpromoted)
.set_i64(Param::GroupNameTimestamp, smeared_time(context));
chat.param.remove(Param::Unpromoted);
chat.update_param(context).await?;
sync_qr_code_tokens = true;
} else {
@@ -3925,8 +3944,6 @@ pub(crate) async fn add_contact_to_chat_ex(
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into());
msg.param
.set_int(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
send_msg(context, chat_id, &mut msg).await?;
sync = Nosync;
@@ -4124,8 +4141,6 @@ pub async fn remove_contact_from_chat(
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
msg.param
.set(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
let res = send_msg(context, chat_id, &mut msg).await;
if contact_id == ContactId::SELF {
res?;
@@ -4343,7 +4358,6 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msg.param.remove(Param::WebxdcDocumentTimestamp);
msg.param.remove(Param::WebxdcSummary);
msg.param.remove(Param::WebxdcSummaryTimestamp);
msg.param.remove(Param::IsEdited);
msg.in_reply_to = None;
// do not leak data as group names; a default subject is generated by mimefactory
@@ -4353,6 +4367,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
let new_msg_id = chat
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
.await?;
msg.rfc724_mid = create_outgoing_rfc724_mid();
curr_timestamp += 1;
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
@@ -4404,7 +4420,7 @@ pub(crate) async fn save_copy_in_self_talk(
bail!("message already saved.");
}
let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, \
let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, txt_raw, \
mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
let row_id = context
.sql
@@ -4598,7 +4614,17 @@ pub async fn add_device_msg_with_importance(
// makes sure, the added message is the last one,
// even if the date is wrong (useful esp. when warning about bad dates)
let mut timestamp_sort = timestamp_sent;
if let Some(last_msg_time) = chat_id.get_timestamp(context).await? {
if let Some(last_msg_time) = context
.sql
.query_get_value(
"SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=?
HAVING COUNT(*) > 0",
(chat_id,),
)
.await?
{
if timestamp_sort <= last_msg_time {
timestamp_sort = last_msg_time + 1;
}
@@ -4725,17 +4751,13 @@ pub(crate) async fn add_info_msg_with_cmd(
timestamp_sent_rcvd: Option<i64>,
parent: Option<&Message>,
from_id: Option<ContactId>,
added_removed_id: Option<ContactId>,
) -> Result<MsgId> {
let rfc724_mid = create_outgoing_rfc724_mid();
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
let mut param = Params::new();
if cmd != SystemMessage::Unknown {
param.set_cmd(cmd);
}
if let Some(contact_id) = added_removed_id {
param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
param.set_cmd(cmd)
}
let row_id =
@@ -4783,7 +4805,6 @@ pub(crate) async fn add_info_msg(
None,
None,
None,
None,
)
.await
}

View File

@@ -1,7 +1,6 @@
use super::*;
use crate::chatlist::get_archived_cnt;
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::ephemeral::Timer;
use crate::headerdef::HeaderDef;
use crate::imex::{has_backup, imex, ImexMode};
use crate::message::{delete_msgs, MessengerMessage};
@@ -299,11 +298,9 @@ async fn test_member_add_remove() -> Result<()> {
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
// Create contact for Bob on the Alice side with name "robert".
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
alice_bob_contact_id.set_name(&alice, "robert").await?;
let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?;
// Set Bob authname to "Bob" and send it to Alice.
bob.set_config(Config::Displayname, Some("Bob")).await?;
@@ -323,35 +320,42 @@ async fn test_member_add_remove() -> Result<()> {
// Create and promote a group.
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
alice
let sent = alice
.send_text(alice_chat_id, "Hi! I created a group.")
.await;
assert!(sent.payload.contains("Hi! I created a group."));
// Alice adds Bob to the chat.
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await;
assert!(sent
.payload
.contains("I added member Bob (bob@example.net)."));
// Locally set name "robert" should not leak.
assert!(!sent.payload.contains("robert"));
assert_eq!(
sent.load_from_db().await.get_text(),
"You added member robert."
"You added member robert (bob@example.net)."
);
// Alice removes Bob from the chat.
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await;
assert!(sent
.payload
.contains("I removed member Bob (bob@example.net)."));
assert!(!sent.payload.contains("robert"));
assert_eq!(
sent.load_from_db().await.get_text(),
"You removed member robert."
"You removed member robert (bob@example.net)."
);
// Alice leaves the chat.
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
let sent = alice.pop_sent_msg().await;
assert!(sent.payload.contains("I left the group."));
assert_eq!(sent.load_from_db().await.get_text(), "You left the group.");
Ok(())
@@ -364,12 +368,10 @@ async fn test_parallel_member_remove() -> Result<()> {
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let charlie = tcm.charlie().await;
let fiona = tcm.fiona().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(&charlie).await;
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
let alice_claire_contact_id = Contact::create(&alice, "Claire", "claire@example.net").await?;
// Create and promote a group.
let alice_chat_id =
@@ -384,8 +386,8 @@ async fn test_parallel_member_remove() -> Result<()> {
let bob_chat_id = bob_received_msg.get_chat_id();
bob_chat_id.accept(&bob).await?;
// Alice adds Charlie to the chat.
add_contact_to_chat(&alice, alice_chat_id, alice_charlie_contact_id).await?;
// Alice adds Claire to the chat.
add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
let alice_sent_add_msg = alice.pop_sent_msg().await;
// Bob leaves the chat.
@@ -414,7 +416,7 @@ async fn test_parallel_member_remove() -> Result<()> {
// Test that remove message is rewritten.
assert_eq!(
bob_received_remove_msg.get_text(),
"Member Me removed by alice@example.org."
"Member Me (bob@example.net) removed by alice@example.org."
);
Ok(())
@@ -426,10 +428,11 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(&fiona).await;
let alice_bob_contact_id =
Contact::create(&alice, "Bob", &bob.get_config(Config::Addr).await?.unwrap()).await?;
let fiona_addr = "fiona@example.net";
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", fiona_addr).await?;
let bob_fiona_contact_id = Contact::create(&bob, "Fiona", fiona_addr).await?;
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
@@ -471,9 +474,8 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_modify_chat_multi_device() -> Result<()> {
let mut tcm = TestContextManager::new();
let a1 = tcm.alice().await;
let a2 = tcm.alice().await;
let a1 = TestContext::new_alice().await;
let a2 = TestContext::new_alice().await;
a1.set_config_bool(Config::BccSelf, true).await?;
// create group and sync it to the second device
@@ -497,9 +499,8 @@ async fn test_modify_chat_multi_device() -> Result<()> {
assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 1);
// add a member to the group
let bob = tcm.bob().await;
let bob_id = a1.add_or_lookup_contact_id(&bob).await;
add_contact_to_chat(&a1, a1_chat_id, bob_id).await?;
let bob = Contact::create(&a1, "", "bob@example.org").await?;
add_contact_to_chat(&a1, a1_chat_id, bob).await?;
let a1_msg = a1.get_last_msg().await;
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
@@ -523,19 +524,11 @@ async fn test_modify_chat_multi_device() -> Result<()> {
assert!(a2_msg.is_system_message());
assert_eq!(a1_msg.get_info_type(), SystemMessage::GroupNameChanged);
assert_eq!(a2_msg.get_info_type(), SystemMessage::GroupNameChanged);
assert_eq!(
a1_msg.get_info_contact_id(&a1).await?,
Some(ContactId::SELF)
);
assert_eq!(
a2_msg.get_info_contact_id(&a2).await?,
Some(ContactId::SELF)
);
assert_eq!(Chat::load_from_db(&a1, a1_chat_id).await?.name, "bar");
assert_eq!(Chat::load_from_db(&a2, a2_chat_id).await?.name, "bar");
// remove member from group
remove_contact_from_chat(&a1, a1_chat_id, bob_id).await?;
remove_contact_from_chat(&a1, a1_chat_id, bob).await?;
let a1_msg = a1.get_last_msg().await;
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
@@ -562,18 +555,13 @@ async fn test_modify_chat_multi_device() -> Result<()> {
async fn test_modify_chat_disordered() -> Result<()> {
let _n = TimeShiftFalsePositiveNote;
let mut tcm = TestContextManager::new();
// Alice creates a group with Bob, Charlie and Fiona and then removes Charlie and Fiona
// Alice creates a group with Bob, Claire and Daisy and then removes Claire and Daisy
// (time shift is needed as otherwise smeared time from Alice looks to Bob like messages from the future which are all set to "now" then)
let alice = tcm.alice().await;
let alice = TestContext::new_alice().await;
let bob = tcm.bob().await;
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
let charlie = tcm.charlie().await;
let charlie_id = alice.add_or_lookup_contact_id(&charlie).await;
let fiona = tcm.fiona().await;
let fiona_id = alice.add_or_lookup_contact_id(&fiona).await;
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
let claire_id = Contact::create(&alice, "", "claire@foo.de").await?;
let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
@@ -581,21 +569,21 @@ async fn test_modify_chat_disordered() -> Result<()> {
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
let add1 = alice.pop_sent_msg().await;
add_contact_to_chat(&alice, alice_chat_id, charlie_id).await?;
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
let add2 = alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_millis(1100));
add_contact_to_chat(&alice, alice_chat_id, fiona_id).await?;
add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?;
let add3 = alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_millis(1100));
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
remove_contact_from_chat(&alice, alice_chat_id, charlie_id).await?;
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
let remove1 = alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_millis(1100));
remove_contact_from_chat(&alice, alice_chat_id, fiona_id).await?;
remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?;
let remove2 = alice.pop_sent_msg().await;
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
@@ -646,27 +634,24 @@ async fn test_modify_chat_lost() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
let charlie = tcm.charlie().await;
let charlie_id = alice.add_or_lookup_contact_id(&charlie).await;
let fiona = tcm.fiona().await;
let fiona_id = alice.add_or_lookup_contact_id(&fiona).await;
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
let claire_id = Contact::create(&alice, "", "claire@foo.de").await?;
let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(&alice, alice_chat_id, charlie_id).await?;
add_contact_to_chat(&alice, alice_chat_id, fiona_id).await?;
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let add = alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_millis(1100));
remove_contact_from_chat(&alice, alice_chat_id, charlie_id).await?;
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
let remove1 = alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_millis(1100));
remove_contact_from_chat(&alice, alice_chat_id, fiona_id).await?;
remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?;
let remove2 = alice.pop_sent_msg().await;
let bob = tcm.bob().await;
@@ -689,13 +674,12 @@ async fn test_modify_chat_lost() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_leave_group() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// Create group chat with Bob.
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
let bob_contact = alice.add_or_lookup_contact(&bob).await.id;
let bob_contact = Contact::create(&alice, "", "bob@example.net").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?;
// Alice sends first message to group.
@@ -1408,52 +1392,20 @@ async fn test_pinned_after_new_msgs() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_chat_name() {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo")
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
assert_eq!(
Chat::load_from_db(alice, chat_id).await.unwrap().get_name(),
Chat::load_from_db(&t, chat_id).await.unwrap().get_name(),
"foo"
);
set_chat_name(alice, chat_id, "bar").await.unwrap();
set_chat_name(&t, chat_id, "bar").await.unwrap();
assert_eq!(
Chat::load_from_db(alice, chat_id).await.unwrap().get_name(),
Chat::load_from_db(&t, chat_id).await.unwrap().get_name(),
"bar"
);
let bob = &tcm.bob().await;
let bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(alice, chat_id, bob_contact_id)
.await
.unwrap();
let sent_msg = alice.send_text(chat_id, "Hi").await;
let received_msg = bob.recv_msg(&sent_msg).await;
let bob_chat_id = received_msg.chat_id;
for new_name in [
"Baz",
"xyzzy",
"Quux",
"another name",
"something different",
] {
set_chat_name(alice, chat_id, new_name).await.unwrap();
let sent_msg = alice.pop_sent_msg().await;
let received_msg = bob.recv_msg(&sent_msg).await;
assert_eq!(received_msg.chat_id, bob_chat_id);
assert_eq!(
Chat::load_from_db(bob, bob_chat_id)
.await
.unwrap()
.get_name(),
new_name
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1584,7 +1536,6 @@ async fn test_add_info_msg_with_cmd() -> Result<()> {
None,
None,
None,
None,
)
.await?;
@@ -1664,6 +1615,58 @@ async fn test_lookup_self_by_contact_id() {
assert_eq!(chat.blocked, Blocked::Not);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_group_with_removed_message_id() -> Result<()> {
// Alice creates a group with Bob, sends a message to bob
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_bob_contact = alice.add_or_lookup_contact(&bob).await;
let contact_id = alice_bob_contact.id;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await?;
add_contact_to_chat(&alice, alice_chat_id, contact_id).await?;
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
send_text_msg(&alice, alice_chat_id, "hi!".to_string()).await?;
assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 1);
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
let sent_msg = alice.pop_sent_msg().await;
let msg = sent_msg.payload();
assert_eq!(msg.match_indices("Message-ID: <").count(), 2);
assert_eq!(msg.match_indices("References: <").count(), 1);
let msg = msg.replace("Message-ID: <", "Message-ID: <X.X");
assert_eq!(msg.match_indices("References: <").count(), 1);
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
let msg = bob.get_last_msg().await;
let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(bob_chat.grpid, alice_chat.grpid);
// Bob accepts contact request.
bob_chat.id.unblock(&bob).await?;
// Bob answers - simulate a normal MUA by not setting `Chat-*`-headers;
// moreover, Bob's SMTP-server also replaces the `Message-ID:`-header
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
let msg = sent_msg.payload();
let msg = msg.replace("Message-ID: <", "Message-ID: <X.X");
let msg = msg.replace("Chat-", "XXXX-");
assert_eq!(msg.match_indices("Chat-").count(), 0);
// Alice receives this message - she can still detect the group by the `References:`-header
receive_imf(&alice, msg.as_bytes(), false).await.unwrap();
let msg = alice.get_last_msg().await;
assert_eq!(msg.chat_id, alice_chat_id);
assert_eq!(msg.text, "ho!".to_string());
assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 2);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_marknoticed_chat() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -2028,22 +2031,20 @@ async fn test_forward() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_info_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let t = TestContext::new_alice().await;
let chat_id1 = create_group_chat(alice, ProtectionStatus::Unprotected, "a").await?;
send_text_msg(alice, chat_id1, "msg one".to_string()).await?;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(alice, chat_id1, bob_id).await?;
let msg1 = alice.get_last_msg_in(chat_id1).await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a").await?;
send_text_msg(&t, chat_id1, "msg one".to_string()).await?;
let bob_id = Contact::create(&t, "", "bob@example.net").await?;
add_contact_to_chat(&t, chat_id1, bob_id).await?;
let msg1 = t.get_last_msg_in(chat_id1).await;
assert!(msg1.is_info());
assert!(msg1.get_text().contains("bob@example.net"));
let chat_id2 = ChatId::create_for_contact(alice, bob_id).await?;
assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), 0);
forward_msgs(alice, &[msg1.id], chat_id2).await?;
let msg2 = alice.get_last_msg_in(chat_id2).await;
let chat_id2 = ChatId::create_for_contact(&t, bob_id).await?;
assert_eq!(get_chat_msgs(&t, chat_id2).await?.len(), 0);
forward_msgs(&t, &[msg1.id], chat_id2).await?;
let msg2 = t.get_last_msg_in(chat_id2).await;
assert!(!msg2.is_info()); // forwarded info-messages lose their info-state
assert_eq!(msg2.get_info_type(), SystemMessage::Unknown);
assert_ne!(msg2.from_id, ContactId::INFO);
@@ -2090,10 +2091,8 @@ async fn test_forward_quote() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_group() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let charlie = tcm.charlie().await;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
@@ -2101,12 +2100,12 @@ async fn test_forward_group() -> Result<()> {
// Alice creates a group with Bob.
let alice_group_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
let charlie_id = alice.add_or_lookup_contact_id(&charlie).await;
let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
let claire_id = Contact::create(&alice, "Claire", "claire@example.net").await?;
add_contact_to_chat(&alice, alice_group_chat_id, bob_id).await?;
add_contact_to_chat(&alice, alice_group_chat_id, charlie_id).await?;
add_contact_to_chat(&alice, alice_group_chat_id, claire_id).await?;
let sent_group_msg = alice
.send_text(alice_group_chat_id, "Hi Bob and Charlie")
.send_text(alice_group_chat_id, "Hi Bob and Claire")
.await;
let bob_group_chat_id = bob.recv_msg(&sent_group_msg).await.chat_id;
@@ -2294,29 +2293,6 @@ async fn test_forward_from_saved_to_saved() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_encrypted_to_unencrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let txt = "This should be encrypted";
let sent = alice.send_text(alice.create_chat(bob).await.id, txt).await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.text, txt);
assert!(msg.get_showpadlock());
let unencrypted_chat = bob.create_email_chat(charlie).await;
forward_msgs(bob, &[msg.id], unencrypted_chat.id).await?;
let msg2 = bob.get_last_msg().await;
assert_eq!(msg2.text, txt);
assert_ne!(msg.id, msg2.id);
assert!(!msg2.get_showpadlock());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_save_from_saved_to_saved_failing() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -2338,13 +2314,11 @@ async fn test_save_from_saved_to_saved_failing() -> Result<()> {
async fn test_resend_own_message() -> Result<()> {
// Alice creates group with Bob and sends an initial message
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let fiona = TestContext::new_fiona().await;
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
add_contact_to_chat(
&alice,
alice_grp,
alice.add_or_lookup_contact_id(&bob).await,
Contact::create(&alice, "", "bob@example.net").await?,
)
.await?;
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
@@ -2353,7 +2327,7 @@ async fn test_resend_own_message() -> Result<()> {
add_contact_to_chat(
&alice,
alice_grp,
alice.add_or_lookup_contact_id(&fiona).await,
Contact::create(&alice, "", "claire@example.org").await?,
)
.await?;
let sent2 = alice.pop_sent_msg().await;
@@ -2397,13 +2371,15 @@ async fn test_resend_own_message() -> Result<()> {
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
// Fiona does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
fiona.recv_msg(&sent2).await;
let msg = fiona.recv_msg(&sent3).await;
// Claire does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
let claire = TestContext::new().await;
claire.configure_addr("claire@example.org").await;
claire.recv_msg(&sent2).await;
let msg = claire.recv_msg(&sent3).await;
assert_eq!(msg.get_text(), "alice->bob");
assert_eq!(get_chat_contacts(&fiona, msg.chat_id).await?.len(), 3);
assert_eq!(get_chat_msgs(&fiona, msg.chat_id).await?.len(), 2);
let msg_from = Contact::get_by_id(&fiona, msg.get_from_id()).await?;
assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3);
assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2);
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
assert_eq!(msg_from.get_addr(), "alice@example.org");
assert!(sent1_ts_sent < msg.timestamp_sent);
@@ -2412,15 +2388,19 @@ async fn test_resend_own_message() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resend_foreign_message_fails() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
let alice = TestContext::new_alice().await;
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
add_contact_to_chat(
&alice,
alice_grp,
Contact::create(&alice, "", "bob@example.net").await?,
)
.await?;
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
let bob = TestContext::new_bob().await;
let msg = bob.recv_msg(&sent1).await;
assert!(resend_msgs(bob, &[msg.id]).await.is_err());
assert!(resend_msgs(&bob, &[msg.id]).await.is_err());
Ok(())
}
@@ -2464,23 +2444,24 @@ async fn test_resend_opportunistically_encryption() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resend_info_message_fails() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
let alice = TestContext::new_alice().await;
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
add_contact_to_chat(
&alice,
alice_grp,
Contact::create(&alice, "", "bob@example.net").await?,
)
.await?;
alice.send_text(alice_grp, "alice->bob").await;
add_contact_to_chat(
alice,
&alice,
alice_grp,
alice.add_or_lookup_contact_id(charlie).await,
Contact::create(&alice, "", "claire@example.org").await?,
)
.await?;
let sent2 = alice.pop_sent_msg().await;
assert!(resend_msgs(alice, &[sent2.sender_msg_id]).await.is_err());
assert!(resend_msgs(&alice, &[sent2.sender_msg_id]).await.is_err());
Ok(())
}
@@ -2516,7 +2497,6 @@ async fn test_broadcast() -> Result<()> {
// create two context, send two messages so both know the other
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let fiona = TestContext::new_fiona().await;
let chat_alice = alice.create_chat(&bob).await;
send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
@@ -2535,8 +2515,6 @@ async fn test_broadcast() -> Result<()> {
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
)
.await?;
let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
add_contact_to_chat(&alice, broadcast_id, fiona_contact_id).await?;
set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
{
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
@@ -2551,16 +2529,11 @@ async fn test_broadcast() -> Result<()> {
{
let sent_msg = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent_msg).await;
assert!(msg.was_encrypted());
assert!(msg
.get_header(HeaderDef::ChatGroupMemberTimestamps)
.is_none());
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_none());
assert!(!sent_msg.payload.contains("Chat-Group-Member-Timestamps:"));
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_text(), "ola!");
assert_eq!(msg.subject, "Broadcast list");
assert!(msg.get_showpadlock());
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Mailinglist);
assert_ne!(chat.id, chat_bob.id);
@@ -2890,7 +2863,12 @@ async fn test_blob_renaming() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(&alice, chat_id, alice.add_or_lookup_contact_id(&bob).await).await?;
add_contact_to_chat(
&alice,
chat_id,
Contact::create(&alice, "bob", "bob@example.net").await?,
)
.await?;
let file = alice.get_blobdir().join("harmless_file.\u{202e}txt.exe");
fs::write(&file, "aaa").await?;
let mut msg = Message::new(Viewtype::File);
@@ -2922,7 +2900,7 @@ async fn test_sync_blocked() -> Result<()> {
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
alice1.recv_msg(&sent_msg).await;
let a0b_contact_id = alice0.add_or_lookup_contact_id(&bob).await;
let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id;
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(alice0).await?;
@@ -2977,13 +2955,12 @@ async fn test_sync_blocked() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_accept_before_first_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let bob = &tcm.bob().await;
let bob = TestContext::new_bob().await;
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
@@ -2997,7 +2974,7 @@ async fn test_sync_accept_before_first_msg() -> Result<()> {
a0b_chat_id.accept(alice0).await?;
let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?;
assert_eq!(a0b_contact.origin, Origin::CreateChat);
assert_eq!(alice0.get_chat(bob).await.blocked, Blocked::Not);
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Not);
sync(alice0, alice1).await;
let alice1_contacts = Contact::get_all(alice1, 0, None).await?;
@@ -3006,7 +2983,7 @@ async fn test_sync_accept_before_first_msg() -> Result<()> {
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
assert_eq!(a1b_contact.get_addr(), "bob@example.net");
assert_eq!(a1b_contact.origin, Origin::CreateChat);
let a1b_chat = alice1.get_chat(bob).await;
let a1b_chat = alice1.get_chat(&bob).await;
assert_eq!(a1b_chat.blocked, Blocked::Not);
let chats = Chatlist::try_load(alice1, 0, None, None).await?;
assert_eq!(chats.len(), 1);
@@ -3142,9 +3119,8 @@ async fn test_sync_adhoc_grp() -> Result<()> {
/// - That sync messages don't unarchive the self-chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_visibility() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
@@ -3196,9 +3172,8 @@ async fn test_sync_device_messages_visibility() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_muted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
@@ -3232,9 +3207,8 @@ async fn test_sync_muted() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_broadcast() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
@@ -3370,128 +3344,6 @@ async fn test_do_not_overwrite_draft() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_info_contact_id() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice2 = &tcm.alice().await;
let bob = &tcm.bob().await;
async fn pop_recv_and_check(
alice: &TestContext,
alice2: &TestContext,
bob: &TestContext,
expected_type: SystemMessage,
expected_alice_id: ContactId,
expected_bob_id: ContactId,
) -> Result<()> {
let sent_msg = alice.pop_sent_msg().await;
let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?;
assert_eq!(msg.get_info_type(), expected_type);
assert_eq!(
msg.get_info_contact_id(alice).await?,
Some(expected_alice_id)
);
let msg = alice2.recv_msg(&sent_msg).await;
assert_eq!(msg.get_info_type(), expected_type);
assert_eq!(
msg.get_info_contact_id(alice2).await?,
Some(expected_alice_id)
);
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_info_type(), expected_type);
assert_eq!(msg.get_info_contact_id(bob).await?, Some(expected_bob_id));
Ok(())
}
// Alice creates group, Bob receives group
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "play", &[bob])
.await;
let sent_msg1 = alice.send_text(alice_chat_id, "moin").await;
let msg = bob.recv_msg(&sent_msg1).await;
let bob_alice_id = msg.from_id;
assert!(!bob_alice_id.is_special());
// Alice does group changes, Bob receives them
set_chat_name(alice, alice_chat_id, "games").await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::GroupNameChanged,
ContactId::SELF,
bob_alice_id,
)
.await?;
let file = alice.get_blobdir().join("avatar.png");
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
set_chat_profile_image(alice, alice_chat_id, file.to_str().unwrap()).await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::GroupImageChanged,
ContactId::SELF,
bob_alice_id,
)
.await?;
alice_chat_id
.set_ephemeral_timer(alice, Timer::Enabled { duration: 60 })
.await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::EphemeralTimerChanged,
ContactId::SELF,
bob_alice_id,
)
.await?;
let fiona_id = alice.add_or_lookup_contact_id(&tcm.fiona().await).await; // contexts are in sync, fiona_id is same everywhere
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::MemberAddedToGroup,
fiona_id,
fiona_id,
)
.await?;
remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::MemberRemovedFromGroup,
fiona_id,
fiona_id,
)
.await?;
// When fiona_id is deleted, get_info_contact_id() returns None.
// We raw delete in db as Contact::delete() leaves a tombstone (which is great as the tap works longer then)
alice
.sql
.execute("DELETE FROM contacts WHERE id=?", (fiona_id,))
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_info_type(), SystemMessage::MemberRemovedFromGroup);
assert!(msg.get_info_contact_id(alice).await?.is_none());
Ok(())
}
/// Test group consistency.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_member_bug() -> Result<()> {
@@ -3499,10 +3351,9 @@ async fn test_add_member_bug() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
// Create a group.
let alice_chat_id =
@@ -3546,8 +3397,7 @@ async fn test_past_members() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let fiona = &tcm.fiona().await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3559,7 +3409,8 @@ async fn test_past_members() -> Result<()> {
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1);
let bob = &tcm.bob().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let add_message = alice.pop_sent_msg().await;
@@ -3578,7 +3429,8 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3590,8 +3442,7 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
let bob_chat_id = bob_received_msg.get_chat_id();
bob_chat_id.accept(bob).await?;
let fiona = &tcm.fiona().await;
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await;
let bob_fiona_contact_id = Contact::create(bob, "Fiona", "fiona@example.net").await?;
// Alice removes Bob and Bob adds Fiona at the same time.
remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
@@ -3613,10 +3464,11 @@ async fn unpromoted_group_no_tombstones() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
let fiona_addr = "fiona@example.net";
let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3632,6 +3484,10 @@ async fn unpromoted_group_no_tombstones() -> Result<()> {
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
let sent = alice.send_text(alice_chat_id, "Hello group!").await;
let payload = sent.payload();
assert_eq!(payload.contains("Hello group!"), true);
assert_eq!(payload.contains(&bob_addr), true);
assert_eq!(payload.contains(fiona_addr), false);
let bob_msg = bob.recv_msg(&sent).await;
let bob_chat_id = bob_msg.chat_id;
@@ -3647,8 +3503,8 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let fiona = &tcm.fiona().await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
let fiona_addr = "fiona@example.net";
let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3663,10 +3519,12 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
let bob = &tcm.bob().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let add_message = alice.pop_sent_msg().await;
assert_eq!(add_message.payload.contains(fiona_addr), false);
let bob_add_message = bob.recv_msg(&add_message).await;
let bob_chat_id = bob_add_message.chat_id;
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
@@ -3678,15 +3536,11 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
/// Test that past members are ordered by the timestamp of their removal.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_past_members_order() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let t = &TestContext::new_alice().await;
let bob = tcm.bob().await;
let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
let charlie = tcm.charlie().await;
let charlie_contact_id = t.add_or_lookup_contact_id(&charlie).await;
let fiona = tcm.fiona().await;
let fiona_contact_id = t.add_or_lookup_contact_id(&fiona).await;
let bob_contact_id = Contact::create(t, "Bob", "bob@example.net").await?;
let charlie_contact_id = Contact::create(t, "Charlie", "charlie@example.org").await?;
let fiona_contact_id = Contact::create(t, "Fiona", "fiona@example.net").await?;
let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "Group chat").await?;
add_contact_to_chat(t, chat_id, bob_contact_id).await?;
@@ -3744,11 +3598,13 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let fiona = &tcm.fiona().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
let charlie_addr = "charlie@example.com";
let alice_charlie_contact_id = Contact::create(alice, "Charlie", charlie_addr).await?;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3770,6 +3626,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1);
let remove_message = alice.pop_sent_msg().await;
assert_eq!(remove_message.payload.contains(charlie_addr), true);
bob.recv_msg(&remove_message).await;
// 60 days pass.
@@ -3778,7 +3635,8 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
// Bob adds Fiona to the chat.
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await;
let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap();
let bob_fiona_contact_id = Contact::create(bob, "Fiona", &fiona_addr).await?;
add_contact_to_chat(bob, bob_chat_id, bob_fiona_contact_id).await?;
let add_message = bob.pop_sent_msg().await;
@@ -3842,7 +3700,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
// Charlie is not part of the chat.
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
assert_eq!(get_past_chat_contacts(bob, bob_chat_id).await?.len(), 0);
let bob_charlie_contact_id = bob.add_or_lookup_contact_id(charlie).await;
let bob_charlie_contact_id = Contact::create(bob, "Charlie", charlie_addr).await?;
assert!(!is_contact_in_chat(bob, bob_chat_id, bob_charlie_contact_id).await?);
assert_eq!(get_chat_contacts(fiona, fiona_chat_id).await?.len(), 3);
@@ -3884,7 +3742,6 @@ async fn test_send_edit_request() -> Result<()> {
bob.recv_msg_opt(&sent2).await;
let test = Message::load_from_db(bob, bob_msg.id).await?;
assert_eq!(test.text, "Text me on Delta.Chat");
assert!(test.is_edited());
// alice has another device, and sees the correction also there
let alice2 = tcm.alice().await;
@@ -3894,12 +3751,6 @@ async fn test_send_edit_request() -> Result<()> {
alice2.recv_msg_opt(&sent2).await;
let test = Message::load_from_db(&alice2, alice2_msg.id).await?;
assert_eq!(test.text, "Text me on Delta.Chat");
assert!(test.is_edited());
// Alice forwards the edited message, the new message shouldn't have the "edited" mark.
forward_msgs(&alice2, &[test.id], test.chat_id).await?;
let forwarded = alice2.get_last_msg().await;
assert!(!forwarded.is_edited());
Ok(())
}

View File

@@ -1,7 +1,7 @@
//! # Chat list module.
use anyhow::{ensure, Context as _, Result};
use std::sync::LazyLock;
use once_cell::sync::Lazy;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
@@ -17,8 +17,8 @@ use crate::summary::Summary;
use crate::tools::IsNoneOrEmpty;
/// Regex to find out if a query should filter by unread messages.
pub static IS_UNREAD_FILTER: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
pub static IS_UNREAD_FILTER: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
/// An object representing a single chatlist in memory.
///
@@ -492,20 +492,19 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_try_load() {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
let chat_id1 = create_group_chat(bob, ProtectionStatus::Unprotected, "a chat")
let t = TestContext::new_bob().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chat_id2 = create_group_chat(bob, ProtectionStatus::Unprotected, "b chat")
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
.await
.unwrap();
let chat_id3 = create_group_chat(bob, ProtectionStatus::Unprotected, "c chat")
let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
.await
.unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3);
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id3);
assert_eq!(chats.get_chat_id(1).unwrap(), chat_id2);
@@ -521,49 +520,51 @@ mod tests {
// 2s here.
for chat_id in &[chat_id1, chat_id3, chat_id2] {
let mut msg = Message::new_text("hello".to_string());
chat_id.set_draft(bob, Some(&mut msg)).await.unwrap();
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
}
let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id2);
// check chatlist query and archive functionality
let chats = Chatlist::try_load(bob, 0, Some("b"), None).await.unwrap();
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
assert_eq!(chats.len(), 1);
// receive a message from alice
let alice = &tcm.alice().await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat")
let alice = TestContext::new_alice().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat")
.await
.unwrap();
add_contact_to_chat(
alice,
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(bob).await,
Contact::create(&alice, "bob", "bob@example.net")
.await
.unwrap(),
)
.await
.unwrap();
send_text_msg(alice, alice_chat_id, "hi".into())
send_text_msg(&alice, alice_chat_id, "hi".into())
.await
.unwrap();
let sent_msg = alice.pop_sent_msg().await;
bob.recv_msg(&sent_msg).await;
let chats = Chatlist::try_load(bob, 0, Some("is:unread"), None)
t.recv_msg(&sent_msg).await;
let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(bob, DC_GCL_ARCHIVED_ONLY, None, None)
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 0);
chat_id1
.set_visibility(bob, ChatVisibility::Archived)
.set_visibility(&t, ChatVisibility::Archived)
.await
.ok();
let chats = Chatlist::try_load(bob, DC_GCL_ARCHIVED_ONLY, None, None)
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 1);

View File

@@ -182,6 +182,12 @@ pub enum Config {
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
MediaQuality,
/// If set to "1", on the first time `start_io()` is called after configuring,
/// the newest existing messages are fetched.
/// Existing recipients are added to the contact database regardless of this setting.
#[strum(props(default = "0"))]
FetchExistingMsgs,
/// If set to "1", then existing messages are considered to be already fetched.
/// This flag is reset after successful configuration.
#[strum(props(default = "1"))]
@@ -475,10 +481,7 @@ impl Config {
/// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool {
matches!(
self,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
)
matches!(self, Config::OnlyFetchMvbox | Config::SentboxWatch)
}
}
@@ -704,6 +707,7 @@ impl Context {
| Config::SentboxWatch
| Config::MvboxMove
| Config::OnlyFetchMvbox
| Config::FetchExistingMsgs
| Config::DeleteToTrash
| Config::Configured
| Config::Bot

View File

@@ -28,15 +28,13 @@ use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::imap::Imap;
use crate::log::LogExt;
pub use crate::login_param::EnteredLoginParam;
use crate::login_param::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate, EnteredCertificateChecks, ProxyConfig,
ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam,
};
use crate::message::Message;
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::qr::set_account_from_qr;
use crate::smtp::Smtp;
use crate::sync::Sync::*;
use crate::tools::time;
@@ -66,59 +64,8 @@ impl Context {
self.sql.get_raw_config_bool("configured").await
}
/// Configures this account with the currently provided parameters.
///
/// Deprecated since 2025-02; use `add_transport_from_qr()`
/// or `add_transport()` instead.
/// Configures this account with the currently set parameters.
pub async fn configure(&self) -> Result<()> {
let param = EnteredLoginParam::load(self).await?;
self.add_transport_inner(&param).await
}
/// Configures a new email account using the provided parameters
/// and adds it as a transport.
///
/// If the email address is the same as an existing transport,
/// then this existing account will be reconfigured instead of a new one being added.
///
/// This function stops and starts IO as needed.
///
/// Usually it will be enough to only set `addr` and `imap.password`,
/// and all the other settings will be autoconfigured.
///
/// During configuration, ConfigureProgress events are emitted;
/// they indicate a successful configuration as well as errors
/// and may be used to create a progress bar.
/// This function will return after configuration is finished.
///
/// If configuration is successful,
/// the working server parameters will be saved
/// and used for connecting to the server.
/// The parameters entered by the user will be saved separately
/// so that they can be prefilled when the user opens the server-configuration screen again.
///
/// See also:
/// - [Self::is_configured()] to check whether there is
/// at least one working transport.
/// - [Self::add_transport_from_qr()] to add a transport
/// from a server encoded in a QR code.
/// - [Self::list_transports()] to get a list of all configured transports.
/// - [Self::delete_transport()] to remove a transport.
pub async fn add_transport(&self, param: &EnteredLoginParam) -> Result<()> {
self.stop_io().await;
let result = self.add_transport_inner(param).await;
if result.is_err() {
if let Ok(true) = self.is_configured().await {
self.start_io().await;
}
return result;
}
self.start_io().await;
Ok(())
}
async fn add_transport_inner(&self, param: &EnteredLoginParam) -> Result<()> {
ensure!(
!self.scheduler.is_running().await,
"cannot configure, already running"
@@ -127,77 +74,42 @@ impl Context {
self.sql.is_open().await,
"cannot configure, database not opened."
);
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), &param.addr) {
bail!("Changing your email address is not supported right now. Check back in a few months!");
}
let cancel_channel = self.alloc_ongoing().await?;
let res = self
.inner_configure(param)
.inner_configure()
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
.await;
self.free_ongoing().await;
if let Err(err) = res.as_ref() {
// We are using Anyhow's .context() and to show the
// inner error, too, we need the {:#}:
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await;
progress!(self, 0, Some(error_msg.clone()));
bail!(error_msg);
progress!(
self,
0,
Some(
stock_str::configuration_failed(
self,
// We are using Anyhow's .context() and to show the
// inner error, too, we need the {:#}:
&format!("{err:#}"),
)
.await
)
);
} else {
param.save(self).await?;
progress!(self, 1000);
}
res
}
/// Adds a new email account as a transport
/// using the server encoded in the QR code.
/// See [Self::add_transport].
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
self.stop_io().await;
let result = async move {
set_account_from_qr(self, qr).await?;
self.configure().await?;
Ok(())
}
.await;
if result.is_err() {
if let Ok(true) = self.is_configured().await {
self.start_io().await;
}
return result;
}
self.start_io().await;
Ok(())
}
/// Returns the list of all email accounts that are used as a transport in the current profile.
/// Use [Self::add_transport()] to add or change a transport
/// and [Self::delete_transport()] to delete a transport.
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
let param = EnteredLoginParam::load(self).await?;
Ok(vec![param])
}
/// Removes the transport with the specified email address
/// (i.e. [EnteredLoginParam::addr]).
#[expect(clippy::unused_async)]
pub async fn delete_transport(&self, _addr: &str) -> Result<()> {
bail!("Adding and removing additional transports is not supported yet. Check back in a few months!")
}
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
async fn inner_configure(&self) -> Result<()> {
info!(self, "Configure ...");
let param = EnteredLoginParam::load(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
let configured_param = configure(self, param).await?;
let configured_param = configure(self, &param).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, configured_param, old_addr).await?;
@@ -273,7 +185,8 @@ async fn get_configured_param(
param.smtp.password.clone()
};
let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?;
let proxy_config = param.proxy_config.clone();
let proxy_enabled = proxy_config.is_some();
let mut addr = param.addr.clone();
if param.oauth2 {
@@ -432,6 +345,7 @@ async fn get_configured_param(
.collect(),
smtp_user: param.smtp.user.clone(),
smtp_password,
proxy_config: param.proxy_config.clone(),
provider,
certificate_checks: match param.certificate_checks {
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
@@ -453,8 +367,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
let configured_param = get_configured_param(ctx, param).await?;
let proxy_config = ProxyConfig::load(ctx).await?;
let strict_tls = configured_param.strict_tls(proxy_config.is_some());
let strict_tls = configured_param.strict_tls();
progress!(ctx, 550);
@@ -464,15 +377,15 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
let smtp_param = configured_param.smtp.clone();
let smtp_password = configured_param.smtp_password.clone();
let smtp_addr = configured_param.addr.clone();
let proxy_config = configured_param.proxy_config.clone();
let proxy_config2 = proxy_config.clone();
let smtp_config_task = task::spawn(async move {
let mut smtp = Smtp::new();
smtp.connect(
&context_smtp,
&smtp_param,
&smtp_password,
&proxy_config2,
&proxy_config,
&smtp_addr,
strict_tls,
configured_param.oauth2,
@@ -490,7 +403,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
let mut imap = Imap::new(
configured_param.imap.clone(),
configured_param.imap_password.clone(),
proxy_config,
configured_param.proxy_config.clone(),
&configured_param.addr,
strict_tls,
configured_param.oauth2,
@@ -499,10 +412,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
let configuring = true;
let mut imap_session = match imap.connect(ctx, configuring).await {
Ok(session) => session,
Err(err) => bail!(
"{}",
nicer_configuration_error(ctx, format!("{err:#}")).await
),
Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await),
};
progress!(ctx, 850);

View File

@@ -2,16 +2,14 @@
#![allow(missing_docs)]
use std::sync::LazyLock;
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC};
use serde::{Deserialize, Serialize};
use crate::chat::ChatId;
pub static DC_VERSION_STR: LazyLock<String> =
LazyLock::new(|| env!("CARGO_PKG_VERSION").to_string());
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
/// Set of characters to percent-encode in email addresses and names.
pub(crate) const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
@@ -180,6 +178,9 @@ pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
/// if none of these flags are set, the default is chosen
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
/// How many existing messages shall be fetched after configuration.
pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
// max. weight of images to send w/o recoding
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
pub const WORSE_IMAGE_BYTES: usize = 130_000;
@@ -220,19 +221,6 @@ pub(crate) const SECUREJOIN_WAIT_TIMEOUT: u64 = 15;
// Newer Delta Chats will remove the prefix as needed.
pub(crate) const EDITED_PREFIX: &str = "✏️";
// Strings needed to render the Autocrypt Setup Message.
// Left untranslated as not being supported/recommended workflow and as translations would require deep knowledge.
pub(crate) const ASM_SUBJECT: &str = "Autocrypt Setup Message";
pub(crate) const ASM_BODY: &str = "This is the Autocrypt Setup Message \
used to transfer your end-to-end setup between clients.
To decrypt and use your setup, \
open the message in an Autocrypt-compliant client \
and enter the setup code presented on the generating device.
If you see this message in a chatmail client (Delta Chat, Arcane Chat, Delta Touch ...), \
use \"Settings / Add Second Device\" instead.";
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;

View File

@@ -95,50 +95,6 @@ impl ContactId {
self.0
}
/// Sets display name for existing contact.
///
/// Display name may be an empty string,
/// in which case the name displayed in the UI
/// for this contact will switch to the
/// contact's authorized name.
pub async fn set_name(self, context: &Context, name: &str) -> Result<()> {
let addr = context
.sql
.transaction(|transaction| {
let is_changed = transaction.execute(
"UPDATE contacts SET name=?1 WHERE id=?2 AND name!=?1",
(name, self),
)? > 0;
if is_changed {
update_chat_names(context, transaction, self)?;
let addr = transaction.query_row(
"SELECT addr FROM contacts WHERE id=?",
(self,),
|row| {
let addr: String = row.get(0)?;
Ok(addr)
},
)?;
Ok(Some(addr))
} else {
Ok(None)
}
})
.await?;
if let Some(addr) = addr {
chat::sync(
context,
chat::SyncId::ContactAddr(addr.to_string()),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
}
Ok(())
}
/// Mark contact as bot.
pub(crate) async fn mark_bot(&self, context: &Context, is_bot: bool) -> Result<()> {
context
@@ -887,48 +843,44 @@ impl Contact {
let mut update_addr = false;
let row_id = context
.sql
.transaction(|transaction| {
let row = transaction
.query_row(
"SELECT id, name, addr, origin, authname
let row_id = context.sql.transaction(|transaction| {
let row = transaction.query_row(
"SELECT id, name, addr, origin, authname
FROM contacts WHERE addr=? COLLATE NOCASE",
(addr,),
|row| {
let row_id: u32 = row.get(0)?;
let row_name: String = row.get(1)?;
let row_addr: String = row.get(2)?;
let row_origin: Origin = row.get(3)?;
let row_authname: String = row.get(4)?;
[addr.to_string()],
|row| {
let row_id: isize = row.get(0)?;
let row_name: String = row.get(1)?;
let row_addr: String = row.get(2)?;
let row_origin: Origin = row.get(3)?;
let row_authname: String = row.get(4)?;
Ok((row_id, row_name, row_addr, row_origin, row_authname))
},
)
.optional()?;
Ok((row_id, row_name, row_addr, row_origin, row_authname))
}).optional()?;
let row_id;
if let Some((id, row_name, row_addr, row_origin, row_authname)) = row {
let update_name = manual && name != row_name;
let update_authname = !manual
&& name != row_authname
&& !name.is_empty()
&& (origin >= row_origin
|| origin == Origin::IncomingUnknownFrom
|| row_authname.is_empty());
let row_id;
if let Some((id, row_name, row_addr, row_origin, row_authname)) = row {
let update_name = manual && name != row_name;
let update_authname = !manual
&& name != row_authname
&& !name.is_empty()
&& (origin >= row_origin
|| origin == Origin::IncomingUnknownFrom
|| row_authname.is_empty());
row_id = id;
if origin >= row_origin && addr.as_ref() != row_addr {
update_addr = true;
}
if update_name || update_authname || update_addr || origin > row_origin {
let new_name = if update_name {
name.to_string()
} else {
row_name
};
row_id = u32::try_from(id)?;
if origin >= row_origin && addr.as_ref() != row_addr {
update_addr = true;
}
if update_name || update_authname || update_addr || origin > row_origin {
let new_name = if update_name {
name.to_string()
} else {
row_name
};
transaction.execute(
transaction
.execute(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
(
new_name,
@@ -947,38 +899,88 @@ impl Contact {
} else {
row_authname
},
row_id,
row_id
),
)?;
if update_name || update_authname {
let contact_id = ContactId::new(row_id);
update_chat_names(context, transaction, contact_id)?;
}
sth_modified = Modifier::Modified;
}
} else {
let update_name = manual;
let update_authname = !manual;
if update_name || update_authname {
// Update the contact name also if it is used as a group name.
// This is one of the few duplicated data, however, getting the chat list is easier this way.
let chat_id: Option<ChatId> = transaction.query_row(
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
(Chattype::Single, isize::try_from(row_id)?),
|row| {
let chat_id: ChatId = row.get(0)?;
Ok(chat_id)
}
).optional()?;
transaction.execute(
if let Some(chat_id) = chat_id {
let contact_id = ContactId::new(row_id);
let (addr, name, authname) =
transaction.query_row(
"SELECT addr, name, authname
FROM contacts
WHERE id=?",
(contact_id,),
|row| {
let addr: String = row.get(0)?;
let name: String = row.get(1)?;
let authname: String = row.get(2)?;
Ok((addr, name, authname))
})?;
let chat_name = if !name.is_empty() {
name
} else if !authname.is_empty() {
authname
} else {
addr
};
let count = transaction.execute(
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
(chat_name, chat_id))?;
if count > 0 {
// Chat name updated
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
}
}
}
sth_modified = Modifier::Modified;
}
} else {
let update_name = manual;
let update_authname = !manual;
transaction
.execute(
"INSERT INTO contacts (name, addr, origin, authname)
VALUES (?, ?, ?, ?);",
(
if update_name { &name } else { "" },
(
if update_name {
name.to_string()
} else {
"".to_string()
},
&addr,
origin,
if update_authname { &name } else { "" },
if update_authname {
name.to_string()
} else {
"".to_string()
}
),
)?;
sth_modified = Modifier::Created;
row_id = u32::try_from(transaction.last_insert_rowid())?;
info!(context, "Added contact id={row_id} addr={addr}.");
}
Ok(row_id)
})
.await?;
sth_modified = Modifier::Created;
row_id = u32::try_from(transaction.last_insert_rowid())?;
info!(context, "Added contact id={row_id} addr={addr}.");
}
Ok(row_id)
}).await?;
let contact_id = ContactId::new(row_id);
@@ -1277,16 +1279,9 @@ impl Contact {
.map(|k| k.dc_fingerprint().to_string())
.unwrap_or_default();
if addr < peerstate.addr {
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
cat_fingerprint(
&mut ret,
&stock_str::self_msg(context).await,
&addr,
&fingerprint_self,
"",
);
cat_fingerprint(
&mut ret,
contact.get_display_name(),
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
@@ -1294,18 +1289,11 @@ impl Contact {
} else {
cat_fingerprint(
&mut ret,
contact.get_display_name(),
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
cat_fingerprint(
&mut ret,
&stock_str::self_msg(context).await,
&addr,
&fingerprint_self,
"",
);
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
}
Ok(ret)
@@ -1406,13 +1394,16 @@ impl Contact {
&self.addr
}
/// Get authorized name or address.
/// Get a summary of authorized name and address.
///
/// The returned string is either "Name (email@domain.com)" or just
/// "email@domain.com" if the name is unset.
///
/// This string is suitable for sending over email
/// as it does not leak the locally set name.
pub(crate) fn get_authname_or_addr(&self) -> String {
pub fn get_authname_n_addr(&self) -> String {
if !self.authname.is_empty() {
(&self.authname).into()
format!("{} ({})", self.authname, self.addr)
} else {
(&self.addr).into()
}
@@ -1615,60 +1606,6 @@ impl Contact {
}
}
// Updates the names of the chats which use the contact name.
//
// This is one of the few duplicated data, however, getting the chat list is easier this way.
fn update_chat_names(
context: &Context,
transaction: &rusqlite::Connection,
contact_id: ContactId,
) -> Result<()> {
let chat_id: Option<ChatId> = transaction.query_row(
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
(Chattype::Single, contact_id),
|row| {
let chat_id: ChatId = row.get(0)?;
Ok(chat_id)
}
).optional()?;
if let Some(chat_id) = chat_id {
let (addr, name, authname) = transaction.query_row(
"SELECT addr, name, authname
FROM contacts
WHERE id=?",
(contact_id,),
|row| {
let addr: String = row.get(0)?;
let name: String = row.get(1)?;
let authname: String = row.get(2)?;
Ok((addr, name, authname))
},
)?;
let chat_name = if !name.is_empty() {
name
} else if !authname.is_empty() {
authname
} else {
addr
};
let count = transaction.execute(
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
(chat_name, chat_id),
)?;
if count > 0 {
// Chat name updated
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
}
}
Ok(())
}
pub(crate) async fn set_blocked(
context: &Context,
sync: sync::Sync,
@@ -1859,14 +1796,12 @@ pub(crate) async fn update_last_seen(
fn cat_fingerprint(
ret: &mut String,
name: &str,
addr: &str,
fingerprint_verified: &str,
fingerprint_unverified: &str,
) {
*ret += &format!(
"\n\n{} ({}):\n{}",
name,
"\n\n{}:\n{}",
addr,
if !fingerprint_verified.is_empty() {
fingerprint_verified
@@ -1878,7 +1813,7 @@ fn cat_fingerprint(
&& !fingerprint_unverified.is_empty()
&& fingerprint_verified != fingerprint_unverified
{
*ret += &format!("\n\n{name} (alternative):\n{fingerprint_unverified}");
*ret += &format!("\n\n{addr} (alternative):\n{fingerprint_unverified}");
}
}

View File

@@ -763,11 +763,11 @@ async fn test_contact_get_encrinfo() -> Result<()> {
"End-to-end encryption preferred.
Fingerprints:
Me (alice@example.org):
alice@example.org:
2E6F A2CB 23B5 32D7 2863
4B58 64B0 8F61 A9ED 9443
Bob (bob@example.net):
bob@example.net:
CCCB 5AA9 F6E1 141C 9431
65F1 DB18 B18C BCF7 0487"
);
@@ -1050,9 +1050,8 @@ async fn test_sync_create() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_make_n_import_vcard() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice = &TestContext::new_alice().await;
let bob = &TestContext::new_bob().await;
bob.set_config(Config::Displayname, Some("Bob")).await?;
let avatar_path = bob.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");

View File

@@ -903,6 +903,12 @@ impl Context {
}
res.insert("secondary_addrs", secondary_addrs);
res.insert(
"fetch_existing_msgs",
self.get_config_int(Config::FetchExistingMsgs)
.await?
.to_string(),
);
res.insert(
"fetched_existing_msgs",
self.get_config_bool(Config::FetchedExistingMsgs)

View File

@@ -3,8 +3,8 @@
//! A module to remove HTML tags from the email text
use std::io::BufRead;
use std::sync::LazyLock;
use once_cell::sync::Lazy;
use quick_xml::{
events::{BytesEnd, BytesStart, BytesText},
Reader,
@@ -176,8 +176,7 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
}
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
static LINE_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds

View File

@@ -220,6 +220,7 @@ impl Session {
vec![uid],
&uid_message_ids,
false,
false,
)
.await?;
if last_uid.is_none() {
@@ -368,6 +369,7 @@ mod tests {
header.as_bytes(),
false,
Some(100000),
false,
)
.await?;
let msg = t.get_last_msg().await;
@@ -383,6 +385,7 @@ mod tests {
format!("{header}\n\n100k text...").as_bytes(),
false,
None,
false,
)
.await?;
let msg = t.get_last_msg().await;
@@ -417,6 +420,7 @@ mod tests {
Content-Type: text/plain",
false,
Some(100000),
false,
)
.await?;
assert_eq!(
@@ -453,6 +457,7 @@ mod tests {
sent2.payload().as_bytes(),
false,
Some(sent2.payload().len() as u32),
false,
)
.await?;
let msg = bob.get_last_msg().await;
@@ -468,6 +473,7 @@ mod tests {
sent2.payload().as_bytes(),
false,
None,
false,
)
.await?;
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
@@ -511,7 +517,15 @@ mod tests {
";
// not downloading the mdn results in an placeholder
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, Some(raw.len() as u32)).await?;
receive_imf_from_inbox(
&bob,
"bar@example.org",
raw,
false,
Some(raw.len() as u32),
false,
)
.await?;
let msg = bob.get_last_msg().await;
let chat_id = msg.chat_id;
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1);
@@ -519,7 +533,7 @@ mod tests {
// downloading the mdn afterwards expands to nothing and deletes the placeholder directly
// (usually mdn are too small for not being downloaded directly)
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None).await?;
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None, false).await?;
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
assert!(Message::load_from_db_optional(&bob, msg.id)
.await?

View File

@@ -1,9 +1,8 @@
//! End-to-end encryption support.
use std::collections::BTreeSet;
use std::io::Cursor;
use anyhow::{bail, Result};
use anyhow::{format_err, Context as _, Result};
use mail_builder::mime::MimePart;
use num_traits::FromPrimitive;
@@ -43,76 +42,79 @@ impl EncryptHelper {
}
/// Determines if we can and should encrypt.
///
/// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by
/// Autocrypt Level 1, version 1.1) and for messages sent in protected groups.
///
/// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
pub(crate) async fn should_encrypt(
&self,
context: &Context,
e2ee_guaranteed: bool,
peerstates: &[(Option<Peerstate>, String)],
) -> Result<bool> {
let is_chatmail = context.is_chatmail().await?;
for (peerstate, _addr) in peerstates {
if let Some(peerstate) = peerstate {
// For chatmail we ignore the encryption preference,
// because we can either send encrypted or not at all.
if is_chatmail || peerstate.prefer_encrypt != EncryptPreference::Reset {
continue;
let mut prefer_encrypt_count = 1;
for (peerstate, addr) in peerstates {
match peerstate {
Some(peerstate) => {
if match peerstate.prefer_encrypt {
EncryptPreference::Reset => is_chatmail,
EncryptPreference::NoPreference | EncryptPreference::Mutual => true,
} {
prefer_encrypt_count += 1;
}
}
None => {
let msg = format!("Peerstate for {addr:?} missing, cannot encrypt");
if e2ee_guaranteed {
return Err(format_err!("{msg}"));
} else {
info!(context, "{msg}.");
return Ok(false);
}
}
}
return Ok(false);
}
Ok(true)
// Count number of recipients, including self.
// This does not depend on whether we send a copy to self or not.
let recipients_count = peerstates.len() + 1;
Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count)
}
/// Constructs a vector of public keys for given peerstates.
///
/// In addition returns the set of recipient addresses
/// for which there is no key available.
///
/// Returns an error if there are recipients
/// other than self, but no recipient keys are available.
pub(crate) fn encryption_keyring(
&self,
/// Tries to encrypt the passed in `mail`.
pub async fn encrypt(
self,
context: &Context,
verified: bool,
peerstates: &[(Option<Peerstate>, String)],
) -> Result<(Vec<SignedPublicKey>, BTreeSet<String>)> {
// Encrypt to self unconditionally,
// even for a single-device setup.
let mut keyring = vec![self.public_key.clone()];
let mut missing_key_addresses = BTreeSet::new();
if peerstates.is_empty() {
return Ok((keyring, missing_key_addresses));
}
mail_to_encrypt: MimePart<'static>,
peerstates: Vec<(Option<Peerstate>, String)>,
compress: bool,
) -> Result<String> {
let mut keyring: Vec<SignedPublicKey> = Vec::new();
let mut verifier_addresses: Vec<&str> = Vec::new();
for (peerstate, addr) in peerstates {
if let Some(peerstate) = peerstate {
if let Some(key) = peerstate.clone().take_key(verified) {
keyring.push(key);
verifier_addresses.push(addr);
} else {
warn!(context, "Encryption key for {addr} is missing.");
missing_key_addresses.insert(addr.clone());
}
} else {
warn!(context, "Peerstate for {addr} is missing.");
missing_key_addresses.insert(addr.clone());
}
for (peerstate, addr) in peerstates
.iter()
.filter_map(|(state, addr)| state.clone().map(|s| (s, addr)))
{
let key = peerstate
.take_key(verified)
.with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?;
keyring.push(key);
verifier_addresses.push(addr);
}
debug_assert!(
!keyring.is_empty(),
"At least our own key is in the keyring"
);
if keyring.len() <= 1 {
bail!("No recipient keys are available, cannot encrypt");
}
// Encrypt to self.
keyring.push(self.public_key.clone());
// Encrypt to secondary verified keys
// if we also encrypt to the introducer ("verifier") of the key.
if verified {
for (peerstate, _addr) in peerstates {
for (peerstate, _addr) in &peerstates {
if let Some(peerstate) = peerstate {
if let (Some(key), Some(verifier)) = (
peerstate.secondary_verified_key.as_ref(),
@@ -126,17 +128,6 @@ impl EncryptHelper {
}
}
Ok((keyring, missing_key_addresses))
}
/// Tries to encrypt the passed in `mail`.
pub async fn encrypt(
self,
context: &Context,
keyring: Vec<SignedPublicKey>,
mail_to_encrypt: MimePart<'static>,
compress: bool,
) -> Result<String> {
let sign_key = load_self_secret_key(context).await?;
let mut raw_message = Vec::new();
@@ -333,17 +324,22 @@ Sent with my Delta Chat Messenger: https://delta.chat";
let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
let ps = new_peerstates(EncryptPreference::NoPreference);
assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
// Own preference is `Mutual` and we have the peer's key.
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
let ps = new_peerstates(EncryptPreference::Reset);
assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
let ps = new_peerstates(EncryptPreference::Mutual);
assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
// test with missing peerstate
let ps = vec![(None, "bob@foo.bar".to_string())];
assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await.is_err());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
Ok(())
}

View File

@@ -5,6 +5,7 @@ use crate::chat::{
};
use crate::config::Config;
use crate::constants::DC_CHAT_ID_ARCHIVED_LINK;
use crate::contact::Contact;
use crate::download::DownloadState;
use crate::location;
use crate::message::markseen_msgs;
@@ -790,7 +791,7 @@ async fn test_ephemeral_timer_non_member() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group name").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;

View File

@@ -285,7 +285,7 @@ mod tests {
use crate::contact::ContactId;
use crate::message::{MessengerMessage, Viewtype};
use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_htmlparse_plain_unspecified() {
@@ -442,25 +442,24 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_html_forwarding() {
// alice receives a non-delta html-message
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice = TestContext::new_alice().await;
let chat = alice
.create_chat_with_contact("", "sender@testrun.org")
.await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
receive_imf(alice, raw, false).await.unwrap();
receive_imf(&alice, raw, false).await.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
assert_ne!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::No);
assert!(!msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
// alice: create chat with bob and forward received html-message there
let chat = alice.create_chat_with_contact("", "bob@example.net").await;
forward_msgs(alice, &[msg.get_id()], chat.get_id())
forward_msgs(&alice, &[msg.get_id()], chat.get_id())
.await
.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
@@ -469,11 +468,11 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
// bob: check that bob also got the html-part of the forwarded message
let bob = &tcm.bob().await;
let bob = TestContext::new_bob().await;
let chat = bob.create_chat_with_contact("", "alice@example.org").await;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(chat.id, msg.chat_id);
@@ -482,7 +481,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(bob).await.unwrap().unwrap();
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
@@ -520,11 +519,10 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_html_forwarding_encrypted() {
let mut tcm = TestContextManager::new();
// Alice receives a non-delta html-message
// (`ShowEmails=AcceptedContacts` lets Alice actually receive non-delta messages for known
// contacts, the contact is marked as known by creating a chat using `chat_with_contact()`)
let alice = &tcm.alice().await;
let alice = TestContext::new_alice().await;
alice
.set_config(Config::ShowEmails, Some("1"))
.await
@@ -533,19 +531,19 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
.create_chat_with_contact("", "sender@testrun.org")
.await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
receive_imf(alice, raw, false).await.unwrap();
receive_imf(&alice, raw, false).await.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
// forward the message to saved-messages,
// this will encrypt the message as new_alice() has set up keys
let chat = alice.get_self_chat().await;
forward_msgs(alice, &[msg.get_id()], chat.get_id())
forward_msgs(&alice, &[msg.get_id()], chat.get_id())
.await
.unwrap();
let msg = alice.pop_sent_msg().await;
// receive the message on another device
let alice = &tcm.alice().await;
let alice = TestContext::new_alice().await;
alice
.set_config(Config::ShowEmails, Some("0"))
.await
@@ -558,39 +556,38 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_html() {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// alice sends a message with html-part to bob
let chat_id = alice.create_chat(bob).await.id;
let chat_id = alice.create_chat(&bob).await.id;
let mut msg = Message::new_text("plain text".to_string());
msg.set_html(Some("<b>html</b> text".to_string()));
assert!(msg.mime_modified);
chat::send_msg(alice, chat_id, &mut msg).await.unwrap();
chat::send_msg(&alice, chat_id, &mut msg).await.unwrap();
// check the message is written correctly to alice's db
let msg = alice.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_text(), "plain text");
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("<b>html</b> text"));
// let bob receive the message
let chat_id = bob.create_chat(alice).await.id;
let chat_id = bob.create_chat(&alice).await.id;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.chat_id, chat_id);
assert_eq!(msg.get_text(), "plain text");
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(bob).await.unwrap().unwrap();
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("<b>html</b> text"));
}

View File

@@ -271,14 +271,12 @@ impl Imap {
let param = ConfiguredLoginParam::load(context)
.await?
.context("Not configured")?;
let proxy_config = ProxyConfig::load(context).await?;
let strict_tls = param.strict_tls(proxy_config.is_some());
let imap = Self::new(
param.imap.clone(),
param.imap_password.clone(),
proxy_config,
param.proxy_config.clone(),
&param.addr,
strict_tls,
param.strict_tls(),
param.oauth2,
idle_interrupt_receiver,
);
@@ -350,11 +348,10 @@ impl Imap {
connection_candidate,
)
.await
.context("IMAP failed to connect")
{
Ok(client) => client,
Err(err) => {
warn!(context, "{err:#}.");
warn!(context, "IMAP failed to connect: {err:#}.");
first_error.get_or_insert(err);
continue;
}
@@ -507,7 +504,7 @@ impl Imap {
}
let msgs_fetched = self
.fetch_new_messages(context, session, watch_folder, folder_meaning)
.fetch_new_messages(context, session, watch_folder, folder_meaning, false)
.await
.context("fetch_new_messages")?;
if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
@@ -535,6 +532,7 @@ impl Imap {
session: &mut Session,
folder: &str,
folder_meaning: FolderMeaning,
fetch_existing_msgs: bool,
) -> Result<bool> {
if should_ignore_folder(context, folder, folder_meaning).await? {
info!(context, "Not fetching from {folder:?}.");
@@ -551,7 +549,7 @@ impl Imap {
return Ok(false);
}
if !session.new_mail {
if !session.new_mail && !fetch_existing_msgs {
info!(context, "No new emails in folder {folder:?}.");
return Ok(false);
}
@@ -560,7 +558,14 @@ impl Imap {
let uid_validity = get_uidvalidity(context, folder).await?;
let old_uid_next = get_uid_next(context, folder).await?;
let msgs = session.prefetch(old_uid_next).await.context("prefetch")?;
let msgs = if fetch_existing_msgs {
session
.prefetch_existing_msgs()
.await
.context("prefetch_existing_msgs")?
} else {
session.prefetch(old_uid_next).await.context("prefetch")?
};
let read_cnt = msgs.len();
let download_limit = context.download_limit().await?;
@@ -713,6 +718,7 @@ impl Imap {
uids_fetch_in_batch.split_off(0),
&uid_message_ids,
fetch_partially,
fetch_existing_msgs,
)
.await
.context("fetch_many_msgs")?;
@@ -777,6 +783,28 @@ impl Imap {
.await
.context("failed to get recipients from the inbox")?;
if context.get_config_bool(Config::FetchExistingMsgs).await? {
for meaning in [
FolderMeaning::Mvbox,
FolderMeaning::Inbox,
FolderMeaning::Sent,
] {
let config = match meaning.to_config() {
Some(c) => c,
None => continue,
};
if let Some(folder) = context.get_config(config).await? {
info!(
context,
"Fetching existing messages from folder {folder:?}."
);
self.fetch_new_messages(context, session, &folder, meaning, true)
.await
.context("could not fetch existing messages")?;
}
}
}
info!(context, "Done fetching existing messages.");
Ok(())
}
@@ -1303,6 +1331,7 @@ impl Session {
/// Returns the last UID fetched successfully and the info about each downloaded message.
/// If the message is incorrect or there is a failure to write a message to the database,
/// it is skipped and the error is logged.
#[expect(clippy::too_many_arguments)]
pub(crate) async fn fetch_many_msgs(
&mut self,
context: &Context,
@@ -1311,6 +1340,7 @@ impl Session {
request_uids: Vec<u32>,
uid_message_ids: &BTreeMap<u32, String>,
fetch_partially: bool,
fetching_existing_messages: bool,
) -> Result<(Option<u32>, Vec<ReceivedMsg>)> {
let mut last_uid = None;
let mut received_msgs = Vec::new();
@@ -1444,6 +1474,7 @@ impl Session {
body,
is_seen,
partial,
fetching_existing_messages,
)
.await
{

View File

@@ -1,3 +1,4 @@
use std::cmp;
use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
@@ -6,6 +7,7 @@ use async_imap::types::Mailbox;
use async_imap::Session as ImapSession;
use futures::TryStreamExt;
use crate::constants::DC_FETCH_EXISTING_MSGS_COUNT;
use crate::imap::capabilities::Capabilities;
use crate::net::session::SessionStream;
@@ -141,4 +143,33 @@ impl Session {
Ok(msgs.into_iter().map(|((_, uid), msg)| (uid, msg)).collect())
}
/// Like prefetch(), but not for new messages but existing ones (the DC_FETCH_EXISTING_MSGS_COUNT newest messages)
pub(crate) async fn prefetch_existing_msgs(
&mut self,
) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
let exists: i64 = {
let mailbox = self.selected_mailbox.as_ref().context("no mailbox")?;
mailbox.exists.into()
};
// Fetch last DC_FETCH_EXISTING_MSGS_COUNT (100) messages.
// Sequence numbers are sequential. If there are 1000 messages in the inbox,
// we can fetch the sequence numbers 900-1000 and get the last 100 messages.
let first = cmp::max(1, exists - DC_FETCH_EXISTING_MSGS_COUNT + 1);
let set = format!("{first}:{exists}");
let mut list = self
.fetch(&set, PREFETCH_FLAGS)
.await
.context("IMAP Could not fetch")?;
let mut msgs = BTreeMap::new();
while let Some(msg) = list.try_next().await? {
if let Some(msg_uid) = msg.uid {
msgs.insert((msg.internal_date(), msg_uid), msg);
}
}
Ok(msgs.into_iter().map(|((_, uid), msg)| (uid, msg)).collect())
}
}

View File

@@ -6,7 +6,6 @@ use anyhow::{bail, ensure, Result};
use crate::blob::BlobObject;
use crate::chat::{self, ChatId};
use crate::config::Config;
use crate::constants::{ASM_BODY, ASM_SUBJECT};
use crate::contact::ContactId;
use crate::context::Context;
use crate::imex::set_self_key;
@@ -15,6 +14,7 @@ use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::pgp;
use crate::stock_str;
use crate::tools::open_file_std;
/// Initiates key transfer via Autocrypt Setup Message.
@@ -39,18 +39,18 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
msg.param.set(Param::File, setup_file_blob.as_name());
msg.param
.set(Param::Filename, "autocrypt-setup-message.html");
msg.subject = ASM_SUBJECT.to_owned();
msg.subject = stock_str::ac_setup_msg_subject(context).await;
msg.param
.set(Param::MimeType, "application/autocrypt-setup");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
msg.force_plaintext();
msg.param.set_int(Param::SkipAutocrypt, 1);
chat::send_msg(context, chat_id, &mut msg).await?;
// Enable BCC-self, because transferring a key
// means we have a multi-device setup.
context.set_config_bool(Config::BccSelf, true).await?;
chat::send_msg(context, chat_id, &mut msg).await?;
Ok(setup_code)
}
@@ -113,8 +113,8 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
);
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
let msg_subj = ASM_SUBJECT;
let msg_body = ASM_BODY.to_string();
let msg_subj = stock_str::ac_setup_msg_subject(context).await;
let msg_body = stock_str::ac_setup_msg_body(context).await;
let msg_body_html = msg_body.replace('\r', "").replace('\n', "<br>");
Ok(format!(
concat!(
@@ -187,6 +187,7 @@ mod tests {
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
use crate::test_utils::{TestContext, TestContextManager};
use ::pgp::armor::BlockType;
@@ -212,9 +213,12 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_setup_file_newline_replace() {
let t = TestContext::new_alice().await;
t.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
.await
.unwrap();
let msg = render_setup_file(&t, "pw").await.unwrap();
println!("{}", &msg);
assert!(msg.contains("<p>This is the Autocrypt Setup Message used to transfer your end-to-end setup between clients.<br>"));
assert!(msg.contains("<p>hello<br>there</p>"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -45,7 +45,7 @@ use crate::message::Message;
use crate::qr::Qr;
use crate::stock_str::backup_transfer_msg_body;
use crate::tools::{create_id, time, TempPathGuard};
use crate::{e2ee, EventType};
use crate::EventType;
use super::{export_backup_stream, export_database, import_backup_stream, DBFILE_BACKUP_NAME};
@@ -109,11 +109,6 @@ impl BackupProvider {
.parent()
.context("Context dir not found")?;
// before we export, make sure the private key exists
e2ee::ensure_secret_key_exists(context)
.await
.context("Cannot create private key or private key not available")?;
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
if fs::metadata(&dbfile).await.is_ok() {
fs::remove_file(&dbfile).await?;

View File

@@ -456,13 +456,15 @@ impl std::str::FromStr for Fingerprint {
#[cfg(test)]
mod tests {
use std::sync::{Arc, LazyLock};
use std::sync::Arc;
use once_cell::sync::Lazy;
use super::*;
use crate::config::Config;
use crate::test_utils::{alice_keypair, TestContext};
static KEYPAIR: LazyLock<KeyPair> = LazyLock::new(alice_keypair);
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
#[test]
fn test_from_armored_string() {
@@ -612,6 +614,16 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
assert_eq!(key, key2);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_self_existing() {
let alice = alice_keypair();
let t = TestContext::new_alice().await;
let pubkey = load_self_public_key(&t).await.unwrap();
assert_eq!(alice.public, pubkey);
let seckey = load_self_secret_key(&t).await.unwrap();
assert_eq!(alice.secret, seckey);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_self_generate_public() {
let t = TestContext::new().await;

View File

@@ -68,7 +68,7 @@ mod imap;
pub mod imex;
pub mod key;
pub mod location;
pub mod login_param;
mod login_param;
pub mod message;
mod mimefactory;
pub mod mimeparser;

View File

@@ -4,7 +4,6 @@ use std::fmt;
use anyhow::{format_err, Context as _, Result};
use deltachat_contact_tools::EmailAddress;
use num_traits::ToPrimitive as _;
use serde::{Deserialize, Serialize};
use crate::config::Config;
@@ -12,11 +11,9 @@ use crate::configure::server_params::{expand_param_vector, ServerParams};
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::net::load_connection_timestamp;
pub use crate::net::proxy::ProxyConfig;
pub use crate::provider::Socket;
use crate::provider::{Protocol, Provider, UsernamePattern};
use crate::net::proxy::ProxyConfig;
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
use crate::sql::Sql;
use crate::tools::ToOption;
/// User-entered setting for certificate checks.
///
@@ -47,7 +44,7 @@ pub enum EnteredCertificateChecks {
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub(crate) enum ConfiguredCertificateChecks {
pub enum ConfiguredCertificateChecks {
/// Use configuration from the provider database.
/// If there is no provider database setting for certificate checks,
/// accept invalid certificates.
@@ -119,13 +116,15 @@ pub struct EnteredLoginParam {
/// invalid hostnames
pub certificate_checks: EnteredCertificateChecks,
/// If true, login via OAUTH2 (not recommended anymore)
/// Proxy configuration.
pub proxy_config: Option<ProxyConfig>,
pub oauth2: bool,
}
impl EnteredLoginParam {
/// Loads entered account settings.
pub(crate) async fn load(context: &Context) -> Result<Self> {
pub async fn load(context: &Context) -> Result<Self> {
let addr = context
.get_config(Config::Addr)
.await?
@@ -197,6 +196,8 @@ impl EnteredLoginParam {
.unwrap_or_default();
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
let proxy_config = ProxyConfig::load(context).await?;
Ok(EnteredLoginParam {
addr,
imap: EnteredServerLoginParam {
@@ -214,71 +215,10 @@ impl EnteredLoginParam {
password: send_pw,
},
certificate_checks,
proxy_config,
oauth2,
})
}
/// Saves entered account settings,
/// so that they can be prefilled if the user wants to configure the server again.
pub(crate) async fn save(&self, context: &Context) -> Result<()> {
context.set_config(Config::Addr, Some(&self.addr)).await?;
context
.set_config(Config::MailServer, self.imap.server.to_option())
.await?;
context
.set_config(Config::MailPort, self.imap.port.to_option().as_deref())
.await?;
context
.set_config(
Config::MailSecurity,
self.imap.security.to_i32().to_option().as_deref(),
)
.await?;
context
.set_config(Config::MailUser, self.imap.user.to_option())
.await?;
context
.set_config(Config::MailPw, self.imap.password.to_option())
.await?;
context
.set_config(Config::SendServer, self.smtp.server.to_option())
.await?;
context
.set_config(Config::SendPort, self.smtp.port.to_option().as_deref())
.await?;
context
.set_config(
Config::SendSecurity,
self.smtp.security.to_i32().to_option().as_deref(),
)
.await?;
context
.set_config(Config::SendUser, self.smtp.user.to_option())
.await?;
context
.set_config(Config::SendPw, self.smtp.password.to_option())
.await?;
context
.set_config(
Config::ImapCertificateChecks,
self.certificate_checks.to_i32().to_option().as_deref(),
)
.await?;
let server_flags = if self.oauth2 {
Some(DC_LP_AUTH_OAUTH2.to_string())
} else {
None
};
context
.set_config(Config::ServerFlags, server_flags.as_deref())
.await?;
Ok(())
}
}
impl fmt::Display for EnteredLoginParam {
@@ -379,7 +319,7 @@ impl TryFrom<Socket> for ConnectionSecurity {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ConfiguredServerLoginParam {
pub struct ConfiguredServerLoginParam {
pub connection: ConnectionCandidate,
/// Username.
@@ -417,7 +357,7 @@ pub(crate) async fn prioritize_server_login_params(
/// Login parameters saved to the database
/// after successful configuration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ConfiguredLoginParam {
pub struct ConfiguredLoginParam {
/// `From:` address that was used at the time of configuration.
pub addr: String,
@@ -441,13 +381,15 @@ pub(crate) struct ConfiguredLoginParam {
pub smtp_password: String,
/// Proxy configuration.
pub proxy_config: Option<ProxyConfig>,
pub provider: Option<&'static Provider>,
/// TLS options: whether to allow invalid certificates and/or
/// invalid hostnames
pub certificate_checks: ConfiguredCertificateChecks,
/// If true, login via OAUTH2 (not recommended anymore)
pub oauth2: bool,
}
@@ -486,7 +428,7 @@ impl ConfiguredLoginParam {
/// Load configured account settings from the database.
///
/// Returns `None` if account is not configured.
pub(crate) async fn load(context: &Context) -> Result<Option<Self>> {
pub async fn load(context: &Context) -> Result<Option<Self>> {
if !context.get_config_bool(Config::Configured).await? {
return Ok(None);
}
@@ -739,6 +681,8 @@ impl ConfiguredLoginParam {
}];
}
let proxy_config = ProxyConfig::load(context).await?;
Ok(Some(ConfiguredLoginParam {
addr,
imap,
@@ -749,12 +693,13 @@ impl ConfiguredLoginParam {
smtp_password: send_pw,
certificate_checks,
provider,
proxy_config,
oauth2,
}))
}
/// Save this loginparam to the database.
pub(crate) async fn save_as_configured_params(&self, context: &Context) -> Result<()> {
pub async fn save_as_configured_params(&self, context: &Context) -> Result<()> {
context.set_primary_self_addr(&self.addr).await?;
context
@@ -831,11 +776,11 @@ impl ConfiguredLoginParam {
Ok(())
}
pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool {
pub fn strict_tls(&self) -> bool {
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
match self.certificate_checks {
ConfiguredCertificateChecks::OldAutomatic => {
provider_strict_tls.unwrap_or(connected_through_proxy)
provider_strict_tls.unwrap_or(self.proxy_config.is_some())
}
ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true),
ConfiguredCertificateChecks::Strict => true,
@@ -894,42 +839,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_save_entered_login_param() -> Result<()> {
let t = TestContext::new().await;
let param = EnteredLoginParam {
addr: "alice@example.org".to_string(),
imap: EnteredServerLoginParam {
server: "".to_string(),
port: 0,
security: Socket::Starttls,
user: "".to_string(),
password: "foobar".to_string(),
},
smtp: EnteredServerLoginParam {
server: "".to_string(),
port: 2947,
security: Socket::default(),
user: "".to_string(),
password: "".to_string(),
},
certificate_checks: Default::default(),
oauth2: false,
};
param.save(&t).await?;
assert_eq!(
t.get_config(Config::Addr).await?.unwrap(),
"alice@example.org"
);
assert_eq!(t.get_config(Config::MailPw).await?.unwrap(), "foobar");
assert_eq!(t.get_config(Config::SendPw).await?, None);
assert_eq!(t.get_config_int(Config::SendPort).await?, 2947);
assert_eq!(EnteredLoginParam::load(&t).await?, param);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_save_load_login_param() -> Result<()> {
let t = TestContext::new().await;
@@ -956,6 +865,8 @@ mod tests {
}],
smtp_user: "".to_string(),
smtp_password: "bar".to_string(),
// proxy_config is not saved by `save_to_database`, using default value
proxy_config: None,
provider: None,
certificate_checks: ConfiguredCertificateChecks::Strict,
oauth2: false,
@@ -1058,6 +969,7 @@ mod tests {
],
smtp_user: "alice@posteo.de".to_string(),
smtp_password: "foobarbaz".to_string(),
proxy_config: None,
provider: get_provider_by_id("posteo"),
certificate_checks: ConfiguredCertificateChecks::Strict,
oauth2: false,

View File

@@ -16,7 +16,7 @@ use crate::chat::{send_msg, Chat, ChatId, ChatIdBlocked, ChatVisibility};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL,
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
};
use crate::contact::{self, Contact, ContactId};
use crate::context::Context;
@@ -33,9 +33,10 @@ use crate::reaction::get_msg_reactions;
use crate::sql;
use crate::summary::Summary;
use crate::sync::SyncData;
use crate::tools::create_outgoing_rfc724_mid;
use crate::tools::{
buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
sanitize_filename, time, timestamp_to_str,
sanitize_filename, time, timestamp_to_str, truncate,
};
/// Message ID, including reserved IDs.
@@ -174,6 +175,15 @@ impl MsgId {
self.0
}
/// Returns raw text of a message, used for message info
pub async fn rawtext(self, context: &Context) -> Result<String> {
Ok(context
.sql
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?", (self,))
.await?
.unwrap_or_default())
}
/// Returns server foldernames and UIDs of a message, used for message info
pub async fn get_info_server_urls(
context: &Context,
@@ -210,9 +220,12 @@ impl MsgId {
/// Returns detailed message information in a multi-line text form.
pub async fn get_info(self, context: &Context) -> Result<String> {
let msg = Message::load_from_db(context, self).await?;
let rawtxt: String = self.rawtext(context).await?;
let mut ret = String::new();
let rawtxt = truncate(rawtxt.trim(), DC_DESIRED_TEXT_LEN);
let fts = timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {fts}");
@@ -323,6 +336,9 @@ impl MsgId {
if duration != 0 {
ret += &format!("Duration: {duration} ms\n",);
}
if !rawtxt.is_empty() {
ret += &format!("\n{rawtxt}\n");
}
if !msg.rfc724_mid.is_empty() {
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
@@ -470,6 +486,7 @@ impl Message {
pub fn new(viewtype: Viewtype) -> Self {
Message {
viewtype,
rfc724_mid: create_outgoing_rfc724_mid(),
..Default::default()
}
}
@@ -479,6 +496,7 @@ impl Message {
Message {
viewtype: Viewtype::Text,
text,
rfc724_mid: create_outgoing_rfc724_mid(),
..Default::default()
}
}
@@ -936,51 +954,6 @@ impl Message {
self.param.get_cmd()
}
/// Return the contact ID of the profile to open when tapping the info message.
pub async fn get_info_contact_id(&self, context: &Context) -> Result<Option<ContactId>> {
match self.param.get_cmd() {
SystemMessage::GroupNameChanged
| SystemMessage::GroupImageChanged
| SystemMessage::EphemeralTimerChanged => {
if self.from_id != ContactId::INFO {
Ok(Some(self.from_id))
} else {
Ok(None)
}
}
SystemMessage::MemberAddedToGroup | SystemMessage::MemberRemovedFromGroup => {
if let Some(contact_i32) = self.param.get_int(Param::ContactAddedRemoved) {
let contact_id = ContactId::new(contact_i32.try_into()?);
if contact_id == ContactId::SELF
|| Contact::real_exists_by_id(context, contact_id).await?
{
Ok(Some(contact_id))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
SystemMessage::AutocryptSetupMessage
| SystemMessage::SecurejoinMessage
| SystemMessage::LocationStreamingEnabled
| SystemMessage::LocationOnly
| SystemMessage::ChatProtectionEnabled
| SystemMessage::ChatProtectionDisabled
| SystemMessage::InvalidUnencryptedMail
| SystemMessage::SecurejoinWait
| SystemMessage::SecurejoinWaitTimeout
| SystemMessage::MultiDeviceSync
| SystemMessage::WebxdcStatusUpdate
| SystemMessage::WebxdcInfoMessage
| SystemMessage::IrohNodeAddr
| SystemMessage::Unknown => Ok(None),
}
}
/// Returns true if the message is a system message.
pub fn is_system_message(&self) -> bool {
let cmd = self.param.get_cmd();
@@ -1762,10 +1735,6 @@ pub async fn delete_msgs_ex(
!delete_for_all || msg.from_id == ContactId::SELF,
"Can delete only own messages for others"
);
ensure!(
!delete_for_all || msg.get_showpadlock(),
"Cannot request deletion of unencrypted message for others"
);
modified_chat_ids.insert(msg.chat_id);
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
@@ -2192,9 +2161,11 @@ pub(crate) async fn get_by_rfc724_mids(
let mut latest = None;
for id in mids.iter().rev() {
let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? else {
info!(context, "continue msgid");
continue;
};
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
info!(context, "continue msg");
continue;
};
if msg.download_state == DownloadState::Done {

View File

@@ -147,8 +147,6 @@ async fn test_quote() {
let mut msg = Message::new_text("Quoted message".to_string());
// Send message, so it gets a Message-Id.
assert!(msg.rfc724_mid.is_empty());
let msg_id = chat::send_msg(ctx, chat.id, &mut msg).await.unwrap();
let msg = Message::load_from_db(ctx, msg_id).await.unwrap();
assert!(!msg.rfc724_mid.is_empty());
@@ -358,6 +356,7 @@ async fn test_markseen_msgs() -> Result<()> {
let sent1 = alice.send_msg(alice_chat.id, &mut msg).await;
let msg1 = bob.recv_msg(&sent1).await;
let bob_chat_id = msg1.chat_id;
let mut msg = Message::new_text("this is the text!".to_string());
let sent2 = alice.send_msg(alice_chat.id, &mut msg).await;
let msg2 = bob.recv_msg(&sent2).await;
assert_eq!(msg1.chat_id, msg2.chat_id);
@@ -380,9 +379,11 @@ async fn test_markseen_msgs() -> Result<()> {
// bob sends to alice,
// alice knows bob and messages appear in normal chat
let mut msg = Message::new_text("this is the text!".to_string());
let msg1 = alice
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
.await;
let mut msg = Message::new_text("this is the text!".to_string());
let msg2 = alice
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
.await;

View File

@@ -13,17 +13,14 @@ use mail_builder::headers::HeaderType;
use mail_builder::mime::MimePart;
use tokio::fs;
use crate::aheader::{Aheader, EncryptPreference};
use crate::blob::BlobObject;
use crate::chat::{self, Chat};
use crate::config::Config;
use crate::constants::ASM_SUBJECT;
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::key::DcKey;
use crate::location;
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::{is_hidden, SystemMessage};
@@ -89,15 +86,13 @@ pub struct MimeFactory {
/// Vector of pairs of recipient name and address that goes into the `To` field.
///
/// The list of actual message recipient addresses may be different,
/// e.g. if members are hidden for broadcast lists
/// or if the keys for some recipients are missing
/// and encrypted message cannot be sent to them.
/// e.g. if members are hidden for broadcast lists.
to: Vec<(String, String)>,
/// Vector of pairs of past group member names and addresses.
past_members: Vec<(String, String)>,
/// Timestamps of the members in the same order as in the `to`
/// Timestamps of the members in the same order as in the `recipients`
/// followed by `past_members`.
///
/// If this is not empty, its length
@@ -133,6 +128,7 @@ pub struct RenderedEmail {
pub message: String,
// pub envelope: Envelope,
pub is_encrypted: bool,
pub is_gossiped: bool,
pub last_added_location_id: Option<u32>,
/// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted
@@ -188,6 +184,9 @@ impl MimeFactory {
let mut req_mdn = false;
if chat.is_self_talk() {
if msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
recipients.push(from_addr.to_string());
}
to.push((from_displayname.to_string(), from_addr.to_string()));
} else if chat.is_mailing_list() {
let list_post = chat
@@ -417,10 +416,12 @@ impl MimeFactory {
fn should_force_plaintext(&self) -> bool {
match &self.loaded {
Loaded::Message { msg, .. } => msg
.param
.get_bool(Param::ForcePlaintext)
.unwrap_or_default(),
Loaded::Message { chat, msg } => {
msg.param
.get_bool(Param::ForcePlaintext)
.unwrap_or_default()
|| chat.typ == Chattype::Broadcast
}
Loaded::Mdn { .. } => false,
}
}
@@ -434,6 +435,35 @@ impl MimeFactory {
}
}
async fn should_do_gossip(&self, context: &Context, multiple_recipients: bool) -> Result<bool> {
match &self.loaded {
Loaded::Message { chat, msg } => {
let cmd = msg.param.get_cmd();
if cmd == SystemMessage::MemberAddedToGroup
|| cmd == SystemMessage::SecurejoinMessage
{
Ok(true)
} else if multiple_recipients {
// beside key- and member-changes, force a periodic re-gossip.
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
// `gossip_period == 0` is a special case for testing,
// enabling gossip in every message.
// Otherwise "smeared timestamps" may result in the condition
// to fail even if the clock is monotonic.
if gossip_period == 0 || time() >= gossiped_timestamp + gossip_period {
Ok(true)
} else {
Ok(false)
}
} else {
Ok(false)
}
}
Loaded::Mdn { .. } => Ok(false),
}
}
fn should_attach_profile_data(msg: &Message) -> bool {
msg.param.get_cmd() != SystemMessage::SecurejoinMessage || {
let step = msg.param.get(Param::Arg).unwrap_or_default();
@@ -753,9 +783,13 @@ impl MimeFactory {
}
}
let mut is_gossiped = false;
let peerstates = self.peerstates_for_recipients(context).await?;
let is_encrypted = !self.should_force_plaintext()
&& (e2ee_guaranteed || encrypt_helper.should_encrypt(context, &peerstates).await?);
&& encrypt_helper
.should_encrypt(context, e2ee_guaranteed, &peerstates)
.await?;
let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded {
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
} else {
@@ -920,78 +954,16 @@ impl MimeFactory {
// Add gossip headers in chats with multiple recipients
let multiple_recipients =
peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
let now = time();
match &self.loaded {
Loaded::Message { chat, msg } => {
if chat.typ != Chattype::Broadcast {
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
let Some(key) = peerstate.peek_key(verified) else {
continue;
};
let fingerprint = key.dc_fingerprint().hex();
let cmd = msg.param.get_cmd();
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
|| cmd == SystemMessage::SecurejoinMessage
|| multiple_recipients && {
let gossiped_timestamp: Option<i64> = context
.sql
.query_get_value(
"SELECT timestamp
FROM gossip_timestamp
WHERE chat_id=? AND fingerprint=?",
(chat.id, &fingerprint),
)
.await?;
// `gossip_period == 0` is a special case for testing,
// enabling gossip in every message.
//
// If current time is in the past compared to
// `gossiped_timestamp`, we also gossip because
// either the `gossiped_timestamp` or clock is wrong.
gossip_period == 0
|| gossiped_timestamp
.is_none_or(|ts| now >= ts + gossip_period || now < ts)
};
if !should_do_gossip {
continue;
}
let header = Aheader::new(
peerstate.addr.clone(),
key.clone(),
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included.
EncryptPreference::NoPreference,
)
.to_string();
message = message.header(
"Autocrypt-Gossip",
mail_builder::headers::raw::Raw::new(header),
);
context
.sql
.execute(
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
VALUES (?, ?, ?)
ON CONFLICT (chat_id, fingerprint)
DO UPDATE SET timestamp=excluded.timestamp",
(chat.id, &fingerprint, now),
)
.await?;
}
if self.should_do_gossip(context, multiple_recipients).await? {
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
if let Some(header) = peerstate.render_gossip_header(verified) {
message = message.header(
"Autocrypt-Gossip",
mail_builder::headers::raw::Raw::new(header),
);
is_gossiped = true;
}
}
Loaded::Mdn { .. } => {
// Never gossip in MDNs.
}
}
// Set the appropriate Content-Type for the inner message.
@@ -1013,23 +985,14 @@ impl MimeFactory {
Loaded::Mdn { .. } => true,
};
let (encryption_keyring, missing_key_addresses) =
encrypt_helper.encryption_keyring(context, verified, &peerstates)?;
// XXX: additional newline is needed
// to pass filtermail at
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>
let encrypted = encrypt_helper
.encrypt(context, encryption_keyring, message, compress)
.encrypt(context, verified, message, peerstates, compress)
.await?
+ "\n";
// Remove recipients for which the key is missing.
if !missing_key_addresses.is_empty() {
self.recipients
.retain(|addr| !missing_key_addresses.contains(addr));
}
// Set the appropriate Content-Type for the outer message
MimePart::new(
"multipart/encrypted; protocol=\"application/pgp-encrypted\"",
@@ -1135,6 +1098,7 @@ impl MimeFactory {
message,
// envelope: Envelope::new,
is_encrypted,
is_gossiped,
last_added_location_id,
sync_ids_to_delete: self.sync_ids_to_delete,
rfc724_mid,
@@ -1335,7 +1299,7 @@ impl MimeFactory {
mail_builder::headers::raw::Raw::new("v1").into(),
));
placeholdertext = Some(ASM_SUBJECT.to_string());
placeholdertext = Some(stock_str::ac_setup_msg_body(context).await);
}
SystemMessage::SecurejoinMessage => {
let step = msg.param.get(Param::Arg).unwrap_or_default();

View File

@@ -340,12 +340,11 @@ async fn test_subject_in_group() -> Result<()> {
// 6. Test that in a group, replies also take the quoted message's subject, while non-replies use the group title as subject
let t = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") // TODO encodings, ä
.await
.unwrap();
let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
chat::add_contact_to_chat(&t, group_id, bob_contact_id).await?;
let bob = Contact::create(&t, "", "bob@example.org").await?;
chat::add_contact_to_chat(&t, group_id, bob).await?;
let subject = send_msg_get_subject(&t, group_id, None).await?;
assert_eq!(subject, "groupname");
@@ -728,6 +727,7 @@ async fn test_selfavatar_unencrypted_signed() {
.is_some());
// if another message is sent, that one must not contain the avatar
let mut msg = Message::new_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
@@ -773,24 +773,19 @@ async fn test_selfavatar_unencrypted_signed() {
/// Test that removed member address does not go into the `To:` field.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_remove_member_bcc() -> Result<()> {
let mut tcm = TestContextManager::new();
// Alice creates a group with Bob and Claire and then removes Bob.
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let alice = TestContext::new_alice().await;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
let charlie_contact = Contact::get_by_id(alice, charlie_id).await?;
let charlie_addr = charlie_contact.get_addr();
let claire_addr = "claire@foo.de";
let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
let claire_id = Contact::create(&alice, "Claire", claire_addr).await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, charlie_id).await?;
send_text_msg(alice, alice_chat_id, "Creating a group".to_string()).await?;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
send_text_msg(&alice, alice_chat_id, "Creating a group".to_string()).await?;
remove_contact_from_chat(alice, alice_chat_id, charlie_id).await?;
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
let remove = alice.pop_sent_msg().await;
let remove_payload = remove.payload();
let parsed = mailparse::parse_mail(remove_payload.as_bytes())?;
@@ -802,8 +797,8 @@ async fn test_remove_member_bcc() -> Result<()> {
for to_addr in to.iter() {
match to_addr {
mailparse::MailAddr::Single(ref info) => {
// Addresses should be of existing members (Alice and Bob) and not Charlie.
assert_ne!(info.addr, charlie_addr);
// Addresses should be of existing members (Alice and Bob) and not Claire.
assert_ne!(info.addr, claire_addr);
}
mailparse::MailAddr::Group(_) => {
panic!("Group addresses are not expected here");

View File

@@ -345,13 +345,6 @@ impl MimeMessage {
}
decrypted_msg = Some(msg);
timestamp_sent = Self::get_timestamp_sent(
&decrypted_mail.headers,
timestamp_sent,
timestamp_rcvd,
);
if let Some(protected_aheader_value) = decrypted_mail
.headers
.get_header_value(HeaderDef::Autocrypt)
@@ -423,6 +416,8 @@ impl MimeMessage {
content
});
if let (Ok(mail), true) = (mail, encrypted) {
timestamp_sent =
Self::get_timestamp_sent(&mail.headers, timestamp_sent, timestamp_rcvd);
if !signatures.is_empty() {
// Remove unsigned opportunistically protected headers from messages considered
// Autocrypt-encrypted / displayed with padlock.

View File

@@ -44,13 +44,13 @@ use anyhow::{Context as _, Result};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use std::sync::LazyLock;
use tokio::net::lookup_host;
use tokio::time::timeout;
use super::load_connection_timestamp;
use crate::context::Context;
use crate::tools::time;
use once_cell::sync::Lazy;
/// Inserts entry into DNS cache
/// or updates existing one with a new timestamp.
@@ -90,8 +90,8 @@ pub(crate) async fn prune_dns_cache(context: &Context) -> Result<()> {
/// <https://docs.rs/tokio/1.40.0/tokio/sync/struct.Mutex.html#which-kind-of-mutex-should-you-use>
/// and
/// <https://stackoverflow.com/questions/63712823/why-do-i-get-a-deadlock-when-using-tokio-with-a-stdsyncmutex>.
static LOOKUP_HOST_CACHE: LazyLock<parking_lot::RwLock<HashMap<String, Vec<IpAddr>>>> =
LazyLock::new(Default::default);
static LOOKUP_HOST_CACHE: Lazy<parking_lot::RwLock<HashMap<String, Vec<IpAddr>>>> =
Lazy::new(Default::default);
/// Wrapper for `lookup_host` that returns IP addresses.
async fn lookup_ips(host: impl tokio::net::ToSocketAddrs) -> Result<impl Iterator<Item = IpAddr>> {
@@ -229,7 +229,7 @@ pub(crate) async fn update_connect_timestamp(
///
/// See <https://support.delta.chat/t/no-dns-resolution-result/2778> and
/// <https://github.com/deltachat/deltachat-core-rust/issues/4920> for reasons.
static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new(|| {
static DNS_PRELOAD: Lazy<HashMap<&'static str, Vec<IpAddr>>> = Lazy::new(|| {
HashMap::from([
(
"mail.sangham.net",

View File

@@ -154,21 +154,18 @@ impl Socks5Config {
}
}
/// Configuration for the proxy through which all traffic
/// (except for iroh p2p connections)
/// will be sent.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProxyConfig {
/// HTTP proxy.
// HTTP proxy.
Http(HttpConfig),
/// HTTPS proxy.
// HTTPS proxy.
Https(HttpConfig),
/// SOCKS5 proxy.
// SOCKS5 proxy.
Socks5(Socks5Config),
/// Shadowsocks proxy.
// Shadowsocks proxy.
Shadowsocks(ShadowsocksConfig),
}
@@ -249,7 +246,7 @@ where
impl ProxyConfig {
/// Creates a new proxy configuration by parsing given proxy URL.
pub fn from_url(url: &str) -> Result<Self> {
pub(crate) fn from_url(url: &str) -> Result<Self> {
let url = Url::parse(url).context("Cannot parse proxy URL")?;
match url.scheme() {
"http" => {
@@ -308,7 +305,7 @@ impl ProxyConfig {
///
/// This function can be used to normalize proxy URL
/// by parsing it and serializing back.
pub fn to_url(&self) -> String {
pub(crate) fn to_url(&self) -> String {
match self {
Self::Http(http_config) => http_config.to_url("http"),
Self::Https(http_config) => http_config.to_url("https"),
@@ -394,7 +391,7 @@ impl ProxyConfig {
/// If `load_dns_cache` is true, loads cached DNS resolution results.
/// Use this only if the connection is going to be protected with TLS checks.
pub(crate) async fn connect(
pub async fn connect(
&self,
context: &Context,
target_host: &str,

View File

@@ -215,9 +215,6 @@ pub enum Param {
/// For messages: Message text was edited.
IsEdited = b'L',
/// For info messages: Contact ID in added or removed to a group.
ContactAddedRemoved = b'5',
}
/// An object for handling key=value parameter lists.

View File

@@ -405,6 +405,28 @@ impl Peerstate {
};
}
/// Returns the contents of the `Autocrypt-Gossip` header for outgoing messages.
pub fn render_gossip_header(&self, verified: bool) -> Option<String> {
if let Some(key) = self.peek_key(verified) {
let header = Aheader::new(
self.addr.clone(),
key.clone(), // TODO: avoid cloning
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included,
// but we include it anyway to propagate encryption
// preference to new members in group chats.
if self.last_seen_autocrypt > 0 {
self.prefer_encrypt
} else {
EncryptPreference::NoPreference
},
);
Some(header.to_string())
} else {
None
}
}
/// Converts the peerstate into the contact public key.
///
/// Similar to [`Self::peek_key`], but consumes the peerstate and returns owned key.
@@ -726,7 +748,6 @@ impl Peerstate {
Some(timestamp),
None,
None,
None,
)
.await?;
}

View File

@@ -1,6 +1,7 @@
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
use std::collections::{BTreeMap, HashSet};
use std::io;
use std::io::Cursor;
use anyhow::{bail, Context as _, Result};
@@ -13,8 +14,8 @@ use pgp::composed::{
use pgp::crypto::ecc_curve::ECCCurve;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::types::{CompressionAlgorithm, PublicKeyTrait, StringToKey};
use rand::thread_rng;
use pgp::types::{CompressionAlgorithm, PublicKeyTrait, SignatureBytes, StringToKey};
use rand::{thread_rng, CryptoRng, Rng};
use tokio::runtime::Handle;
use crate::key::{DcKey, Fingerprint};
@@ -30,6 +31,95 @@ const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AE
/// Preferred cryptographic hash.
const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::SHA2_256;
/// A wrapper for rPGP public key types
#[derive(Debug)]
enum SignedPublicKeyOrSubkey<'a> {
Key(&'a SignedPublicKey),
Subkey(&'a SignedPublicSubKey),
}
impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
fn version(&self) -> pgp::types::KeyVersion {
match self {
Self::Key(k) => k.version(),
Self::Subkey(k) => k.version(),
}
}
fn fingerprint(&self) -> pgp::types::Fingerprint {
match self {
Self::Key(k) => k.fingerprint(),
Self::Subkey(k) => k.fingerprint(),
}
}
fn key_id(&self) -> pgp::types::KeyId {
match self {
Self::Key(k) => k.key_id(),
Self::Subkey(k) => k.key_id(),
}
}
fn algorithm(&self) -> pgp::crypto::public_key::PublicKeyAlgorithm {
match self {
Self::Key(k) => k.algorithm(),
Self::Subkey(k) => k.algorithm(),
}
}
fn created_at(&self) -> &chrono::DateTime<chrono::Utc> {
match self {
Self::Key(k) => k.created_at(),
Self::Subkey(k) => k.created_at(),
}
}
fn expiration(&self) -> Option<u16> {
match self {
Self::Key(k) => k.expiration(),
Self::Subkey(k) => k.expiration(),
}
}
fn verify_signature(
&self,
hash: HashAlgorithm,
data: &[u8],
sig: &SignatureBytes,
) -> pgp::errors::Result<()> {
match self {
Self::Key(k) => k.verify_signature(hash, data, sig),
Self::Subkey(k) => k.verify_signature(hash, data, sig),
}
}
fn encrypt<R: Rng + CryptoRng>(
&self,
rng: R,
plain: &[u8],
typ: pgp::types::EskType,
) -> pgp::errors::Result<pgp::types::PkeskBytes> {
match self {
Self::Key(k) => k.encrypt(rng, plain, typ),
Self::Subkey(k) => k.encrypt(rng, plain, typ),
}
}
fn serialize_for_hashing(&self, writer: &mut impl io::Write) -> pgp::errors::Result<()> {
match self {
Self::Key(k) => k.serialize_for_hashing(writer),
Self::Subkey(k) => k.serialize_for_hashing(writer),
}
}
fn public_params(&self) -> &pgp::types::PublicParams {
match self {
Self::Key(k) => k.public_params(),
Self::Subkey(k) => k.public_params(),
}
}
}
/// Split data from PGP Armored Data as defined in <https://tools.ietf.org/html/rfc4880#section-6.2>.
///
/// Returns (type, headers, base64 encoded body).
@@ -146,15 +236,28 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<KeyPair> {
Ok(key_pair)
}
/// Selects a subkey of the public key to use for encryption.
/// Select public key or subkey to use for encryption.
///
/// Returns `None` if the public key cannot be used for encryption.
/// First, tries to use subkeys. If none of the subkeys are suitable
/// for encryption, tries to use primary key. Returns `None` if the public
/// key cannot be used for encryption.
///
/// TODO: take key flags and expiration dates into account
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<SignedPublicKeyOrSubkey> {
key.public_subkeys
.iter()
.find(|subkey| subkey.is_encryption_key())
.map_or_else(
|| {
// No usable subkey found, try primary key
if key.is_encryption_key() {
Some(SignedPublicKeyOrSubkey::Key(key))
} else {
None
}
},
|subkey| Some(SignedPublicKeyOrSubkey::Subkey(subkey)),
)
}
/// Encrypts `plain` text using `public_keys_for_encryption`
@@ -169,10 +272,11 @@ pub async fn pk_encrypt(
Handle::current()
.spawn_blocking(move || {
let pkeys: Vec<&SignedPublicSubKey> = public_keys_for_encryption
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
.iter()
.filter_map(select_pk_for_encryption)
.collect();
let pkeys_refs: Vec<&SignedPublicKeyOrSubkey> = pkeys.iter().collect();
let mut rng = thread_rng();
@@ -183,9 +287,13 @@ pub async fn pk_encrypt(
} else {
signed_msg
};
compressed_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys)?
compressed_msg.encrypt_to_keys_seipdv1(
&mut rng,
SYMMETRIC_KEY_ALGORITHM,
&pkeys_refs,
)?
} else {
lit_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys)?
lit_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
};
let encoded_msg = encrypted_msg.to_armored_string(Default::default())?;
@@ -316,7 +424,7 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use once_cell::sync::Lazy;
use tokio::sync::OnceCell;
use super::*;
@@ -394,7 +502,7 @@ mod tests {
static CLEARTEXT: &[u8] = b"This is a test";
/// Initialised [TestKeys] for tests.
static KEYS: LazyLock<TestKeys> = LazyLock::new(TestKeys::new);
static KEYS: Lazy<TestKeys> = Lazy::new(TestKeys::new);
static CTEXT_SIGNED: OnceCell<String> = OnceCell::const_new();
static CTEXT_UNSIGNED: OnceCell<String> = OnceCell::const_new();

View File

@@ -1,6 +1,6 @@
//! Handle plain text together with some attributes.
use std::sync::LazyLock;
use once_cell::sync::Lazy;
use crate::simplify::remove_message_footer;
@@ -25,10 +25,10 @@ impl PlainText {
/// Convert plain text to HTML.
/// The function handles quotes, links, fixed and floating text paragraphs.
pub fn to_html(&self) -> String {
static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
static LINKIFY_MAIL_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
static LINKIFY_URL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
static LINKIFY_URL_RE: Lazy<regex::Regex> = Lazy::new(|| {
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
});

View File

@@ -8,7 +8,7 @@ use crate::provider::{
};
use std::collections::HashMap;
use std::sync::LazyLock;
use once_cell::sync::Lazy;
// 163.md: 163.com
static P_163: Provider = Provider {
@@ -2406,85 +2406,84 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 533] = [
("zoho.com", &P_ZOHO),
];
pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider>> =
LazyLock::new(|| {
HashMap::from([
("163", &P_163),
("aktivix.org", &P_AKTIVIX_ORG),
("aliyun", &P_ALIYUN),
("aol", &P_AOL),
("arcor.de", &P_ARCOR_DE),
("autistici.org", &P_AUTISTICI_ORG),
("blindzeln.org", &P_BLINDZELN_ORG),
("bluewin.ch", &P_BLUEWIN_CH),
("buzon.uy", &P_BUZON_UY),
("chello.at", &P_CHELLO_AT),
("comcast", &P_COMCAST),
("daleth.cafe", &P_DALETH_CAFE),
("dismail.de", &P_DISMAIL_DE),
("disroot", &P_DISROOT),
("e.email", &P_E_EMAIL),
("espiv.net", &P_ESPIV_NET),
("example.com", &P_EXAMPLE_COM),
("fastmail", &P_FASTMAIL),
("firemail.de", &P_FIREMAIL_DE),
("five.chat", &P_FIVE_CHAT),
("freenet.de", &P_FREENET_DE),
("gmail", &P_GMAIL),
("gmx.net", &P_GMX_NET),
("hermes.radio", &P_HERMES_RADIO),
("hey.com", &P_HEY_COM),
("i.ua", &P_I_UA),
("i3.net", &P_I3_NET),
("icloud", &P_ICLOUD),
("infomaniak.com", &P_INFOMANIAK_COM),
("kolst.com", &P_KOLST_COM),
("kontent.com", &P_KONTENT_COM),
("mail.com", &P_MAIL_COM),
("mail.de", &P_MAIL_DE),
("mail.ru", &P_MAIL_RU),
("mail2tor", &P_MAIL2TOR),
("mailbox.org", &P_MAILBOX_ORG),
("mailo.com", &P_MAILO_COM),
("mehl.cloud", &P_MEHL_CLOUD),
("mehl.store", &P_MEHL_STORE),
("migadu", &P_MIGADU),
("nauta.cu", &P_NAUTA_CU),
("naver", &P_NAVER),
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
("nubo.coop", &P_NUBO_COOP),
("outlook.com", &P_OUTLOOK_COM),
("ouvaton.coop", &P_OUVATON_COOP),
("posteo", &P_POSTEO),
("protonmail", &P_PROTONMAIL),
("purelymail.com", &P_PURELYMAIL_COM),
("qq", &P_QQ),
("rambler.ru", &P_RAMBLER_RU),
("riseup.net", &P_RISEUP_NET),
("rogers.com", &P_ROGERS_COM),
("sonic", &P_SONIC),
("stinpriza.net", &P_STINPRIZA_NET),
("systemausfall.org", &P_SYSTEMAUSFALL_ORG),
("systemli.org", &P_SYSTEMLI_ORG),
("t-online", &P_T_ONLINE),
("testrun", &P_TESTRUN),
("tiscali.it", &P_TISCALI_IT),
("tutanota", &P_TUTANOTA),
("ukr.net", &P_UKR_NET),
("undernet.uy", &P_UNDERNET_UY),
("vfemail", &P_VFEMAIL),
("vivaldi", &P_VIVALDI),
("vk.com", &P_VK_COM),
("vodafone.de", &P_VODAFONE_DE),
("web.de", &P_WEB_DE),
("wkpb.de", &P_WKPB_DE),
("yahoo", &P_YAHOO),
("yandex.ru", &P_YANDEX_RU),
("yggmail", &P_YGGMAIL),
("ziggo.nl", &P_ZIGGO_NL),
("zoho", &P_ZOHO),
])
});
pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| {
HashMap::from([
("163", &P_163),
("aktivix.org", &P_AKTIVIX_ORG),
("aliyun", &P_ALIYUN),
("aol", &P_AOL),
("arcor.de", &P_ARCOR_DE),
("autistici.org", &P_AUTISTICI_ORG),
("blindzeln.org", &P_BLINDZELN_ORG),
("bluewin.ch", &P_BLUEWIN_CH),
("buzon.uy", &P_BUZON_UY),
("chello.at", &P_CHELLO_AT),
("comcast", &P_COMCAST),
("daleth.cafe", &P_DALETH_CAFE),
("dismail.de", &P_DISMAIL_DE),
("disroot", &P_DISROOT),
("e.email", &P_E_EMAIL),
("espiv.net", &P_ESPIV_NET),
("example.com", &P_EXAMPLE_COM),
("fastmail", &P_FASTMAIL),
("firemail.de", &P_FIREMAIL_DE),
("five.chat", &P_FIVE_CHAT),
("freenet.de", &P_FREENET_DE),
("gmail", &P_GMAIL),
("gmx.net", &P_GMX_NET),
("hermes.radio", &P_HERMES_RADIO),
("hey.com", &P_HEY_COM),
("i.ua", &P_I_UA),
("i3.net", &P_I3_NET),
("icloud", &P_ICLOUD),
("infomaniak.com", &P_INFOMANIAK_COM),
("kolst.com", &P_KOLST_COM),
("kontent.com", &P_KONTENT_COM),
("mail.com", &P_MAIL_COM),
("mail.de", &P_MAIL_DE),
("mail.ru", &P_MAIL_RU),
("mail2tor", &P_MAIL2TOR),
("mailbox.org", &P_MAILBOX_ORG),
("mailo.com", &P_MAILO_COM),
("mehl.cloud", &P_MEHL_CLOUD),
("mehl.store", &P_MEHL_STORE),
("migadu", &P_MIGADU),
("nauta.cu", &P_NAUTA_CU),
("naver", &P_NAVER),
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
("nubo.coop", &P_NUBO_COOP),
("outlook.com", &P_OUTLOOK_COM),
("ouvaton.coop", &P_OUVATON_COOP),
("posteo", &P_POSTEO),
("protonmail", &P_PROTONMAIL),
("purelymail.com", &P_PURELYMAIL_COM),
("qq", &P_QQ),
("rambler.ru", &P_RAMBLER_RU),
("riseup.net", &P_RISEUP_NET),
("rogers.com", &P_ROGERS_COM),
("sonic", &P_SONIC),
("stinpriza.net", &P_STINPRIZA_NET),
("systemausfall.org", &P_SYSTEMAUSFALL_ORG),
("systemli.org", &P_SYSTEMLI_ORG),
("t-online", &P_T_ONLINE),
("testrun", &P_TESTRUN),
("tiscali.it", &P_TISCALI_IT),
("tutanota", &P_TUTANOTA),
("ukr.net", &P_UKR_NET),
("undernet.uy", &P_UNDERNET_UY),
("vfemail", &P_VFEMAIL),
("vivaldi", &P_VIVALDI),
("vk.com", &P_VK_COM),
("vodafone.de", &P_VODAFONE_DE),
("web.de", &P_WEB_DE),
("wkpb.de", &P_WKPB_DE),
("yahoo", &P_YAHOO),
("yandex.ru", &P_YANDEX_RU),
("yggmail", &P_YGGMAIL),
("ziggo.nl", &P_ZIGGO_NL),
("zoho", &P_ZOHO),
])
});
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2024, 9, 13).unwrap());
pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 9, 13).unwrap());

View File

@@ -2,11 +2,11 @@
mod dclogin_scheme;
use std::collections::BTreeMap;
use std::sync::LazyLock;
use anyhow::{anyhow, bail, ensure, Context as _, Result};
pub use dclogin_scheme::LoginOptions;
use deltachat_contact_tools::{addr_normalize, may_be_valid_addr, ContactAddress};
use once_cell::sync::Lazy;
use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
@@ -695,7 +695,7 @@ struct CreateAccountErrorResponse {
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
/// download additional information from the contained url and set the parameters.
/// on success, a configure::configure() should be able to log in to the account
pub(crate) async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
let url_str = qr
.get(DCACCOUNT_SCHEME.len()..)
.context("Invalid DCACCOUNT scheme")?;
@@ -915,10 +915,10 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
Qr::from_address(context, name, &addr, None).await
}
static VCARD_NAME_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
static VCARD_EMAIL_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
static VCARD_NAME_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
static VCARD_EMAIL_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
/// Extract address for the vcard scheme.
///

View File

@@ -731,9 +731,8 @@ Here's my footer -- bob@example.net"
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reaction_summary() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice.set_config(Config::Displayname, Some("ALICE")).await?;
bob.set_config(Config::Displayname, Some("BOB")).await?;
let alice_bob_id = alice.add_or_lookup_contact_id(&bob).await;
@@ -900,6 +899,7 @@ Here's my footer -- bob@example.net"
msg_header.as_bytes(),
false,
Some(100000),
false,
)
.await?
.unwrap();
@@ -930,6 +930,7 @@ Here's my footer -- bob@example.net"
msg_full.as_bytes(),
false,
None,
false,
)
.await?;

View File

@@ -2,7 +2,6 @@
use std::collections::HashSet;
use std::iter;
use std::sync::LazyLock;
use anyhow::{Context as _, Result};
use data_encoding::BASE32_NOPAD;
@@ -10,6 +9,7 @@ use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line,
use iroh_gossip::proto::TopicId;
use mailparse::SingleInfo;
use num_traits::FromPrimitive;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::aheader::EncryptPreference;
@@ -24,7 +24,6 @@ use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
use crate::key::DcKey;
use crate::log::LogExt;
use crate::message::{
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
@@ -99,11 +98,12 @@ pub async fn receive_imf(
head.as_bytes(),
seen,
Some(imf_raw.len().try_into()?),
false,
)
.await;
}
}
receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None).await
receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None, false).await
}
/// Emulates reception of a message from "INBOX".
@@ -116,6 +116,7 @@ pub(crate) async fn receive_imf_from_inbox(
imf_raw: &[u8],
seen: bool,
is_partial_download: Option<u32>,
fetching_existing_messages: bool,
) -> Result<Option<ReceivedMsg>> {
receive_imf_inner(
context,
@@ -126,6 +127,7 @@ pub(crate) async fn receive_imf_from_inbox(
imf_raw,
seen,
is_partial_download,
fetching_existing_messages,
)
.await
}
@@ -169,6 +171,7 @@ pub(crate) async fn receive_imf_inner(
imf_raw: &[u8],
seen: bool,
is_partial_download: Option<u32>,
fetching_existing_messages: bool,
) -> Result<Option<ReceivedMsg>> {
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
@@ -402,11 +405,8 @@ pub(crate) async fn receive_imf_inner(
received_msg = None;
}
let verified_encryption = has_verified_encryption(&mime_parser, from_id)?;
if verified_encryption == VerifiedEncryption::Verified {
mark_recipients_as_verified(context, from_id, &to_ids, &mime_parser).await?;
}
let verified_encryption =
has_verified_encryption(context, &mime_parser, from_id, &to_ids).await?;
if verified_encryption == VerifiedEncryption::Verified
&& mime_parser.get_header(HeaderDef::ChatVerified).is_some()
@@ -441,6 +441,7 @@ pub(crate) async fn receive_imf_inner(
seen,
is_partial_download,
replace_msg_id,
fetching_existing_messages,
prevent_rename,
verified_encryption,
)
@@ -453,25 +454,22 @@ pub(crate) async fn receive_imf_inner(
}
// Update gossiped timestamp for the chat if someone else or our other device sent
// Autocrypt-Gossip header to avoid sending Autocrypt-Gossip ourselves
// Autocrypt-Gossip for all recipients in the chat to avoid sending Autocrypt-Gossip ourselves
// and waste traffic.
let chat_id = received_msg.chat_id;
if !chat_id.is_special() {
for gossiped_key in mime_parser.gossiped_keys.values() {
context
.sql
.transaction(move |transaction| {
let fingerprint = gossiped_key.dc_fingerprint().hex();
transaction.execute(
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
VALUES (?, ?, ?)
ON CONFLICT (chat_id, fingerprint)
DO UPDATE SET timestamp=MAX(timestamp, excluded.timestamp)",
(chat_id, &fingerprint, mime_parser.timestamp_sent),
)?;
Ok(())
})
if !chat_id.is_special()
&& mime_parser.recipients.iter().all(|recipient| {
recipient.addr == mime_parser.from.addr
|| mime_parser.gossiped_keys.contains_key(&recipient.addr)
})
{
info!(
context,
"Received message contains Autocrypt-Gossip for all members of {chat_id}, updating timestamp."
);
if chat_id.get_gossiped_timestamp(context).await? < mime_parser.timestamp_sent {
chat_id
.set_gossiped_timestamp(context, mime_parser.timestamp_sent)
.await?;
}
}
@@ -717,10 +715,13 @@ async fn add_parts(
seen: bool,
is_partial_download: Option<u32>,
mut replace_msg_id: Option<MsgId>,
fetching_existing_messages: bool,
prevent_rename: bool,
verified_encryption: VerifiedEncryption,
) -> Result<ReceivedMsg> {
let is_bot = context.get_config_bool(Config::Bot).await?;
// Bots handle existing messages the same way as new ones.
let fetching_existing_messages = fetching_existing_messages && !is_bot;
let rfc724_mid_orig = &mime_parser
.get_rfc724_mid()
.unwrap_or(rfc724_mid.to_string());
@@ -763,10 +764,8 @@ async fn add_parts(
let allow_creation;
if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
&& is_dc_message == MessengerMessage::No
&& !context.get_config_bool(Config::IsChatmail).await?
{
// the message is a classic email in a classic profile
// (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
// this message is a classic email not a chat-message nor a reply to one
match show_emails {
ShowEmails::Off => {
info!(context, "Classical email not shown (TRASH).");
@@ -1041,7 +1040,11 @@ async fn add_parts(
}
}
state = if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
state = if seen
|| fetching_existing_messages
|| is_mdn
|| chat_id_blocked == Blocked::Yes
|| group_changes.silent
// No check for `hidden` because only reactions are such and they should be `InFresh`.
{
MessageState::InSeen
@@ -1108,7 +1111,7 @@ async fn add_parts(
}
}
if mime_parser.decrypting_failed {
if mime_parser.decrypting_failed && !fetching_existing_messages {
if chat_id.is_none() {
chat_id = Some(DC_CHAT_ID_TRASH);
} else {
@@ -1234,6 +1237,12 @@ async fn add_parts(
}
}
if fetching_existing_messages && mime_parser.decrypting_failed {
chat_id = Some(DC_CHAT_ID_TRASH);
// We are only gathering old messages on first start. We do not want to add loads of non-decryptable messages to the chats.
info!(context, "Existing non-decipherable message (TRASH).");
}
if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
if let Some(part) = mime_parser.parts.first() {
if part.typ == Viewtype::Text && part.msg.is_empty() {
@@ -1439,13 +1448,13 @@ async fn add_parts(
None => better_msg = Some(m),
Some(_) => {
if !m.is_empty() {
group_changes.extra_msgs.push((m, is_system_message, None))
group_changes.extra_msgs.push((m, is_system_message))
}
}
}
}
for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
for (group_changes_msg, cmd) in group_changes.extra_msgs {
chat::add_info_msg_with_cmd(
context,
chat_id,
@@ -1455,7 +1464,6 @@ async fn add_parts(
None,
None,
None,
added_removed_id,
)
.await?;
}
@@ -1496,7 +1504,7 @@ async fn add_parts(
while let Some(part) = parts.next() {
if part.is_reaction {
let reaction_str = simplify::remove_footers(part.msg.as_str());
let is_incoming_fresh = mime_parser.incoming && !seen;
let is_incoming_fresh = mime_parser.incoming && !seen && !fetching_existing_messages;
set_msg_reaction(
context,
mime_in_reply_to,
@@ -1528,6 +1536,7 @@ async fn add_parts(
}
}
let mut txt_raw = "".to_string();
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
if better_msg.is_empty() && is_partial_download.is_none() {
chat_id = DC_CHAT_ID_TRASH;
@@ -1539,13 +1548,14 @@ async fn add_parts(
let part_is_empty =
typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
if let Some(contact_id) = group_changes.added_removed_id {
param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
}
save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
let save_mime_modified = save_mime_modified && parts.peek().is_none();
if part.typ == Viewtype::Text {
let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default();
txt_raw = format!("{subject}\n\n{msg_raw}");
}
let ephemeral_timestamp = if in_fresh {
0
} else {
@@ -1572,7 +1582,7 @@ INSERT INTO msgs
rfc724_mid, chat_id,
from_id, to_id, timestamp, timestamp_sent,
timestamp_rcvd, type, state, msgrmsg,
txt, txt_normalized, subject, param, hidden,
txt, txt_normalized, subject, txt_raw, param, hidden,
bytes, mime_headers, mime_compressed, mime_in_reply_to,
mime_references, mime_modified, error, ephemeral_timer,
ephemeral_timestamp, download_state, hop_info
@@ -1581,7 +1591,7 @@ INSERT INTO msgs
?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?, ?, 1,
?, ?, ?, ?,
?, ?, ?, ?
@@ -1591,7 +1601,7 @@ SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
param=excluded.param,
txt_raw=excluded.txt_raw, param=excluded.param,
hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
@@ -1613,6 +1623,8 @@ RETURNING id
if trash || hidden { "" } else { msg },
if trash || hidden { None } else { message::normalize_text(msg) },
if trash || hidden { "" } else { &subject },
// txt_raw might contain invalid utf8
if trash || hidden { "" } else { &txt_raw },
if trash {
"".to_string()
} else {
@@ -2327,12 +2339,10 @@ struct GroupChangesInfo {
/// Optional: A better message that should replace the original system message.
/// If this is an empty string, the original system message should be trashed.
better_msg: Option<String>,
/// Added/removed contact `better_msg` refers to.
added_removed_id: Option<ContactId>,
/// If true, the user should not be notified about the group change.
silent: bool,
/// A list of additional group changes messages that should be shown in the chat.
extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
extra_msgs: Vec<(String, SystemMessage)>,
}
/// Apply group member list, name, avatar and protection status changes from the MIME message.
@@ -2438,16 +2448,16 @@ async fn apply_group_changes(
.filter(|grpname| grpname.len() < 200)
{
let grpname = &sanitize_single_line(grpname);
let old_name = &sanitize_single_line(old_name);
let chat_group_name_timestamp =
chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
// To provide group name consistency, compare names if timestamps are equal.
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, old_name)
&& chat_id
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
.await?
&& grpname != &chat.name
{
info!(context, "Updating grpname for chat {chat_id}.");
context
@@ -2460,7 +2470,6 @@ async fn apply_group_changes(
.get_header(HeaderDef::ChatGroupNameChanged)
.is_some()
{
let old_name = &sanitize_single_line(old_name);
better_msg.get_or_insert(
stock_str::msg_grp_name(context, old_name, grpname, from_id).await,
);
@@ -2650,11 +2659,6 @@ async fn apply_group_changes(
}
Ok(GroupChangesInfo {
better_msg,
added_removed_id: if added_id.is_some() {
added_id
} else {
removed_id
},
silent,
extra_msgs: group_changes_msgs,
})
@@ -2666,7 +2670,7 @@ async fn group_changes_msgs(
added_ids: &HashSet<ContactId>,
removed_ids: &HashSet<ContactId>,
chat_id: ChatId,
) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
) -> Result<Vec<(String, SystemMessage)>> {
let mut group_changes_msgs = Vec::new();
if !added_ids.is_empty() {
warn!(
@@ -2687,7 +2691,6 @@ async fn group_changes_msgs(
stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
.await,
SystemMessage::MemberAddedToGroup,
Some(contact.id),
));
}
for contact_id in removed_ids {
@@ -2696,14 +2699,13 @@ async fn group_changes_msgs(
stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
.await,
SystemMessage::MemberRemovedFromGroup,
Some(contact.id),
));
}
Ok(group_changes_msgs)
}
static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
Ok(match LIST_ID_REGEX.captures(list_id_header) {
@@ -2811,8 +2813,8 @@ fn compute_mailinglist_name(
// (as that part is much more visible, we assume, that names is shorter and comes more to the point,
// than the sometimes longer part from ListId)
let subject = mime_parser.get_subject().unwrap_or_default();
static SUBJECT: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); // remove square brackets around first name
static SUBJECT: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); // remove square brackets around first name
if let Some(cap) = SUBJECT.captures(&subject) {
name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
}
@@ -2839,8 +2841,8 @@ fn compute_mailinglist_name(
// but strip some known, long hash prefixes
if name.is_empty() {
// 51231231231231231231231232869f58.xing.com -> xing.com
static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
static PREFIX_32_CHARS_HEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
if let Some(cap) = PREFIX_32_CHARS_HEX
.captures(listid)
.and_then(|caps| caps.get(2))
@@ -3057,12 +3059,23 @@ async fn update_verified_keys(
/// Checks whether the message is allowed to appear in a protected chat.
///
/// This means that it is encrypted and signed with a verified key.
fn has_verified_encryption(
///
/// Also propagates gossiped keys to verified if needed.
async fn has_verified_encryption(
context: &Context,
mimeparser: &MimeMessage,
from_id: ContactId,
to_ids: &[ContactId],
) -> Result<VerifiedEncryption> {
use VerifiedEncryption::*;
// We do not need to check if we are verified with ourself.
let to_ids = to_ids
.iter()
.copied()
.filter(|id| *id != ContactId::SELF)
.collect::<Vec<ContactId>>();
if !mimeparser.was_encrypted() {
return Ok(NotVerified("This message is not encrypted".to_string()));
};
@@ -3091,24 +3104,21 @@ fn has_verified_encryption(
}
}
mark_recipients_as_verified(context, from_id, to_ids, mimeparser).await?;
Ok(Verified)
}
async fn mark_recipients_as_verified(
context: &Context,
from_id: ContactId,
to_ids: &[ContactId],
to_ids: Vec<ContactId>,
mimeparser: &MimeMessage,
) -> Result<()> {
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
return Ok(());
}
let contact = Contact::get_by_id(context, from_id).await?;
for &id in to_ids {
if id == ContactId::SELF {
continue;
}
for id in to_ids {
let Some((to_addr, is_verified)) = context
.sql
.query_row_optional(
@@ -3206,6 +3216,7 @@ async fn get_parent_message(
if let Some(field) = references {
mids.append(&mut parse_message_ids(field));
}
info!(context, "mids: {mids:?}");
message::get_by_rfc724_mids(context, &mids).await
}

View File

@@ -2218,17 +2218,6 @@ async fn test_no_smtp_job_for_self_chat() -> Result<()> {
let mut msg = Message::new_text("Happy birthday to me".to_string());
chat::send_msg(bob, chat_id, &mut msg).await?;
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
bob.set_config_bool(Config::BccSelf, true).await?;
bob.set_config(Config::DeleteServerAfter, Some("1")).await?;
let mut msg = Message::new_text("Happy birthday to me".to_string());
chat::send_msg(bob, chat_id, &mut msg).await?;
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
bob.set_config(Config::DeleteServerAfter, None).await?;
let mut msg = Message::new_text("Happy birthday to me".to_string());
chat::send_msg(bob, chat_id, &mut msg).await?;
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_some());
Ok(())
}
@@ -3313,7 +3302,6 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
let alice1 = tcm.alice().await;
let alice2 = tcm.alice().await;
let bob = tcm.bob().await;
let charlie = tcm.charlie().await;
// =============== Bob creates a group ===============
let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
@@ -3322,8 +3310,8 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
time(),
group_id,
&[
bob.add_or_lookup_contact_id(&alice1).await,
bob.add_or_lookup_contact_id(&charlie).await,
bob.add_or_lookup_contact(&alice1).await.id,
Contact::create(&bob, "", "charlie@example.org").await?,
],
)
.await?;
@@ -3450,7 +3438,8 @@ async fn test_send_as_bot() -> Result<()> {
let alice = &tcm.alice().await;
alice.set_config(Config::Bot, Some("1")).await.unwrap();
let bob = &tcm.bob().await;
let alice_bob_id = alice.add_or_lookup_contact_id(bob).await;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let alice_bob_id = Contact::create(alice, "", &bob_addr).await?;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
let alice_chat_id = ChatId::lookup_by_contact(alice, alice_bob_id)
.await?
@@ -3466,6 +3455,46 @@ async fn test_send_as_bot() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_bot_recv_existing_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
bob.set_config(Config::Bot, Some("1")).await.unwrap();
bob.set_config(Config::FetchExistingMsgs, Some("1"))
.await
.unwrap();
let fetching_existing_messages = true;
let msg = receive_imf_from_inbox(
bob,
"first@example.org",
b"From: Alice <alice@example.org>\n\
To: Bob <bob@example.net>\n\
Chat-Version: 1.0\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\n\
Content-Type: text/plain\n\
\n\
hello\n",
false,
None,
fetching_existing_messages,
)
.await?
.unwrap();
let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?;
assert_eq!(msg.state, MessageState::InFresh);
let event = bob
.evtracker
.get_matching(|ev| matches!(ev, EventType::IncomingMsg { .. }))
.await;
let EventType::IncomingMsg { chat_id, msg_id } = event else {
unreachable!();
};
assert_eq!(chat_id, msg.chat_id);
assert_eq!(msg_id, msg.id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_wrong_date_in_imf_section() {
let mut tcm = TestContextManager::new();
@@ -4004,7 +4033,7 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let bob_id = Contact::create(alice, "Bob", &bob_addr).await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
@@ -4026,39 +4055,38 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_member_list_on_rejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice = tcm.alice().await;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let fiona_id = alice.add_or_lookup_contact_id(fiona).await;
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
let claire_id = Contact::create(&alice, "", "claire@example.de").await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let add = alice.pop_sent_msg().await;
let bob = tcm.bob().await;
bob.recv_msg(&add).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3);
// remove bob from chat
remove_contact_from_chat(alice, alice_chat_id, bob_id).await?;
remove_contact_from_chat(&alice, alice_chat_id, bob_id).await?;
let remove_bob = alice.pop_sent_msg().await;
bob.recv_msg(&remove_bob).await;
// remove any other member
remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?;
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
alice.pop_sent_msg().await;
// re-add bob
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
let add2 = alice.pop_sent_msg().await;
bob.recv_msg(&add2).await;
// number of members in chat should have updated
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
Ok(())
}
@@ -4070,7 +4098,8 @@ async fn test_ignore_outdated_membership_changes() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_bob_id = alice.add_or_lookup_contact_id(bob).await;
let alice_bob_id =
Contact::create(alice, "", &bob.get_config(Config::Addr).await?.unwrap()).await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
// Alice creates a group chat. Bob accepts it.
@@ -4113,101 +4142,107 @@ async fn test_ignore_outdated_membership_changes() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let charlie = &tcm.charlie().await;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(
alice,
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(bob).await,
Contact::create(&alice, "bob", "bob@example.net").await?,
)
.await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
bob_chat_id.accept(bob).await?;
bob_chat_id.accept(&bob).await?;
// alice adds a member
add_contact_to_chat(
alice,
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(fiona).await,
Contact::create(&alice, "fiona", "fiona@example.net").await?,
)
.await?;
// Bob adds a member.
let bob_charlie = bob.add_or_lookup_contact_id(charlie).await;
add_contact_to_chat(bob, bob_chat_id, bob_charlie).await?;
// bob adds a member.
let bob_blue = Contact::create(&bob, "blue", "blue@example.net").await?;
add_contact_to_chat(&bob, bob_chat_id, bob_blue).await?;
alice.recv_msg(&bob.pop_sent_msg().await).await;
// Bob didn't receive the addition of Fiona, but Alice mustn't remove Fiona from the members
// list back. Instead, Bob must add Fiona from the next Alice's message to make their group
// members view consistent.
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 4);
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
// Just a dumb check for remove_contact_from_chat(). Let's have it in this only place.
remove_contact_from_chat(bob, bob_chat_id, bob_charlie).await?;
remove_contact_from_chat(&bob, bob_chat_id, bob_blue).await?;
alice.recv_msg(&bob.pop_sent_msg().await).await;
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 3);
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
SystemTime::shift(Duration::from_secs(3600));
send_text_msg(alice, alice_chat_id, "Finally add Fiona please".to_string()).await?;
send_text_msg(
&alice,
alice_chat_id,
"Finally add Fiona please".to_string(),
)
.await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delayed_removal_is_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_bob = alice.add_or_lookup_contact_id(bob).await;
let alice_fiona = alice.add_or_lookup_contact_id(fiona).await;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_fiona = Contact::create(&alice, "fiona", "fiona@example.net").await?;
// create chat with three members
add_to_chat_contacts_table(alice, time(), chat_id, &[alice_bob, alice_fiona]).await?;
add_to_chat_contacts_table(
&alice,
time(),
chat_id,
&[
Contact::create(&alice, "bob", "bob@example.net").await?,
alice_fiona,
],
)
.await?;
send_text_msg(alice, chat_id, "populate".to_string()).await?;
send_text_msg(&alice, chat_id, "populate".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
bob_chat_id.accept(bob).await?;
bob_chat_id.accept(&bob).await?;
// Bob removes Fiona.
let bob_contact_fiona = bob.add_or_lookup_contact_id(fiona).await;
remove_contact_from_chat(bob, bob_chat_id, bob_contact_fiona).await?;
let bob_contact_fiona = Contact::create(&bob, "fiona", "fiona@example.net").await?;
remove_contact_from_chat(&bob, bob_chat_id, bob_contact_fiona).await?;
let remove_msg = bob.pop_sent_msg().await;
// Bob adds new members Dom and Elena, but first addition message is lost.
let dom = &tcm.dom().await;
let elena = &tcm.elena().await;
let bob_dom = bob.add_or_lookup_contact_id(dom).await;
add_contact_to_chat(bob, bob_chat_id, bob_dom).await?;
// Bob adds new members "blue" and "orange", but first addition message is lost.
let bob_blue = Contact::create(&bob, "blue", "blue@example.net").await?;
add_contact_to_chat(&bob, bob_chat_id, bob_blue).await?;
bob.pop_sent_msg().await;
let bob_elena = bob.add_or_lookup_contact_id(elena).await;
add_contact_to_chat(bob, bob_chat_id, bob_elena).await?;
let bob_orange = Contact::create(&bob, "orange", "orange@example.net").await?;
add_contact_to_chat(&bob, bob_chat_id, bob_orange).await?;
let add_msg = bob.pop_sent_msg().await;
// Alice only receives the second member addition,
// but this results in addition of both members
// and removal of Fiona.
alice.recv_msg(&add_msg).await;
assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 4);
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4);
// Alice re-adds Fiona.
add_contact_to_chat(alice, chat_id, alice_fiona).await?;
assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 5);
add_contact_to_chat(&alice, chat_id, alice_fiona).await?;
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 5);
// Delayed removal of Fiona by Bob shouldn't remove her.
alice.recv_msg(&remove_msg).await;
assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 5);
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 5);
alice
.golden_test_chat(chat_id, "receive_imf_delayed_removal_is_ignored")
@@ -4219,42 +4254,41 @@ async fn test_delayed_removal_is_ignored() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_readd_with_normal_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(
alice,
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(bob).await,
Contact::create(&alice, "bob", "bob@example.net").await?,
)
.await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
bob_chat_id.accept(bob).await?;
bob_chat_id.accept(&bob).await?;
// Bob leaves, but Alice didn't receive Bob's leave message.
remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?;
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
bob.pop_sent_msg().await;
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 1);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
SystemTime::shift(Duration::from_secs(3600));
add_contact_to_chat(
alice,
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(fiona).await,
Contact::create(&alice, "fiora", "fiora@example.net").await?,
)
.await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
// Bob received a message from Alice, but this should not re-add him to the group.
assert!(!is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?);
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
// Bob got an update that Fiona is added nevertheless.
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
// Bob got an update that fiora is added nevertheless.
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
Ok(())
}
@@ -4458,17 +4492,16 @@ async fn test_mua_can_readd() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_member_left_does_not_create_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(
alice,
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(bob).await,
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
)
.await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
alice.pop_sent_msg().await;
// Bob only received a message of Alice leaving the group.
@@ -4478,7 +4511,7 @@ async fn test_member_left_does_not_create_chat() -> Result<()> {
// especially the chats that were created due to "split group" bugs
// which some members simply deleted and some members left,
// recreating the chat for others.
remove_contact_from_chat(alice, alice_chat_id, ContactId::SELF).await?;
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
Ok(())
@@ -4486,45 +4519,44 @@ async fn test_member_left_does_not_create_chat() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(
alice,
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(bob).await,
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
)
.await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
alice.pop_sent_msg().await;
send_text_msg(alice, alice_chat_id, "second message".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "second message".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
assert!(!bob_chat_id.is_special());
// Bob missed the message adding them, but must recreate the member list.
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
assert!(is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
// But if Bob just left, they mustn't recreate the member list even after missing a message.
bob_chat_id.accept(bob).await?;
remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?;
bob_chat_id.accept(&bob).await?;
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
bob.pop_sent_msg().await;
send_text_msg(alice, alice_chat_id, "3rd message".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "3rd message".to_string()).await?;
alice.pop_sent_msg().await;
send_text_msg(alice, alice_chat_id, "4th message".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "4th message".to_string()).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
assert!(!is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?);
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
// Even if some time passed, Bob must not be re-added back.
SystemTime::shift(Duration::from_secs(3600));
send_text_msg(alice, alice_chat_id, "5th message".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "5th message".to_string()).await?;
alice.pop_sent_msg().await;
send_text_msg(alice, alice_chat_id, "6th message".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "6th message".to_string()).await?;
bob.recv_msg(&alice.pop_sent_msg().await).await;
assert!(!is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?);
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
Ok(())
}
@@ -4537,7 +4569,7 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
add_contact_to_chat(
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(&bob).await,
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
)
.await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
@@ -4547,7 +4579,12 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
add_contact_to_chat(
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(&fiona).await,
Contact::create(
&alice,
"fiona",
&fiona.get_config(Config::Addr).await?.unwrap(),
)
.await?,
)
.await?;
let fiona_chat_id = fiona.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
@@ -4559,7 +4596,12 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
// Bob missed the message adding fiona, but mustn't recreate the member list.
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
let bob_alice_contact = bob.add_or_lookup_contact_id(&alice).await;
let bob_alice_contact = Contact::create(
&bob,
"alice",
&alice.get_config(Config::Addr).await?.unwrap(),
)
.await?;
assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?);
Ok(())
}
@@ -4745,14 +4787,13 @@ async fn test_create_group_with_big_msg() -> Result<()> {
async fn test_partial_group_consistency() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let add = alice.pop_sent_msg().await;
let bob = tcm.bob().await;
bob.recv_msg(&add).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
@@ -4772,6 +4813,7 @@ Content-Type: text/plain
Chat-Group-Member-Added: charlie@example.com",
false,
Some(100000),
false,
)
.await?
.context("no received message")?;
@@ -4786,7 +4828,7 @@ Chat-Group-Member-Added: charlie@example.com",
add_contact_to_chat(
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(&fiona).await,
Contact::create(&alice, "fiona", "fiona@example.net").await?,
)
.await?;
@@ -4809,6 +4851,7 @@ Content-Type: text/plain
Chat-Group-Member-Added: charlie@example.com",
false,
None,
false,
)
.await?
.context("no received message")?;
@@ -4870,10 +4913,11 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap();
mark_as_verified(alice, fiona).await;
let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id;
add_contact_to_chat(alice, group_id, alice_fiona_id).await?;
// The message is not sent to Bob,
// but member is added to the chat locally anyway.
assert!(add_contact_to_chat(alice, group_id, alice_fiona_id)
.await
.is_err());
// Sending the message failed,
// but member is added to the chat locally already.
assert!(is_contact_in_chat(alice, group_id, alice_fiona_id).await?);
let msg = alice.get_last_msg_in(group_id).await;
assert!(msg.is_info());
@@ -4882,6 +4926,10 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
stock_str::msg_add_member_local(alice, &fiona_addr, ContactId::SELF).await
);
// Now the chat has a message "You added member fiona@example.net. [INFO] !!" (with error) that
// may be confusing, but if the error is displayed in UIs, it's more or less ok. This is not a
// normal scenario anyway.
remove_contact_from_chat(alice, group_id, alice_bob_id).await?;
assert!(!is_contact_in_chat(alice, group_id, alice_bob_id).await?);
let msg = alice.get_last_msg_in(group_id).await;
@@ -4890,6 +4938,7 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
msg.get_text(),
stock_str::msg_del_member_local(alice, &bob_addr, ContactId::SELF,).await
);
assert!(msg.error().is_some());
Ok(())
}
@@ -4986,9 +5035,8 @@ async fn test_unarchive_on_member_removal() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let fiona_id = alice.add_or_lookup_contact_id(fiona).await;
let bob_id = Contact::create(alice, "", "bob@example.net").await?;
let fiona_id = Contact::create(alice, "", "fiona@example.net").await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
@@ -5091,11 +5139,11 @@ async fn test_references() -> Result<()> {
alice.set_config_bool(Config::BccSelf, true).await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
let _sent = alice
alice
.send_text(alice_chat_id, "Hi! I created a group.")
.await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await;
let bob_received_msg = bob.recv_msg(&sent).await;
@@ -5357,16 +5405,14 @@ Hello!"
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_rename_chat_on_missing_message() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let charlie = tcm.charlie().await;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_to_chat_contacts_table(
&alice,
time(),
chat_id,
&[alice.add_or_lookup_contact_id(&bob).await],
&[Contact::create(&alice, "bob", "bob@example.net").await?],
)
.await?;
send_text_msg(&alice, chat_id, "populate".to_string()).await?;
@@ -5380,8 +5426,8 @@ async fn test_rename_chat_on_missing_message() -> Result<()> {
bob.pop_sent_msg().await;
// Bob adds a new member.
let bob_charlie = bob.add_or_lookup_contact_id(&charlie).await;
add_contact_to_chat(&bob, bob_chat_id, bob_charlie).await?;
let bob_orange = Contact::create(&bob, "orange", "orange@example.net").await?;
add_contact_to_chat(&bob, bob_chat_id, bob_orange).await?;
let add_msg = bob.pop_sent_msg().await;
// Alice only receives the member addition.
@@ -5391,29 +5437,6 @@ async fn test_rename_chat_on_missing_message() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_rename_chat_after_creating_invite() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
for populate_before_securejoin in [false, true] {
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
SystemTime::shift(Duration::from_secs(60));
chat::set_chat_name(alice, alice_chat_id, "Renamed").await?;
if populate_before_securejoin {
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
alice.pop_sent_msg().await;
}
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
assert_eq!(bob_chat.get_name(), "Renamed");
}
Ok(())
}
/// Tests that creating a group
/// is preferred over assigning message to existing
/// chat based on `In-Reply-To` and `References`.

View File

@@ -1,10 +1,10 @@
//! DC release info.
use chrono::NaiveDate;
use std::sync::LazyLock;
use once_cell::sync::Lazy;
const DATE_STR: &str = include_str!("../release-date.in");
/// Last release date.
pub static DATE: LazyLock<NaiveDate> =
LazyLock::new(|| NaiveDate::parse_from_str(DATE_STR, "%Y-%m-%d").unwrap());
pub static DATE: Lazy<NaiveDate> =
Lazy::new(|| NaiveDate::parse_from_str(DATE_STR, "%Y-%m-%d").unwrap());

View File

@@ -334,6 +334,12 @@ pub(crate) async fn handle_securejoin_handshake(
inviter_progress(context, contact_id, 300);
// for setup-contact, make Alice's one-to-one chat with Bob visible
// (secure-join-information are shown in the group chat)
if !join_vg {
ChatId::create_for_contact(context, contact_id).await?;
}
// Alice -> Bob
send_alice_handshake_msg(
context,
@@ -429,11 +435,6 @@ pub(crate) async fn handle_securejoin_handshake(
}
contact_id.regossip_keys(context).await?;
ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
// for setup-contact, make Alice's one-to-one chat with Bob visible
// (secure-join-information are shown in the group chat)
if !join_vg {
ChatId::create_for_contact(context, contact_id).await?;
}
info!(context, "Auth verified.",);
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
inviter_progress(context, contact_id, 600);

View File

@@ -126,7 +126,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
Some(ts_start),
None,
None,
None,
)
.await?;
chat_id.spawn_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT);

View File

@@ -129,7 +129,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
.await
.unwrap()
.len(),
0
1
);
let sent = alice.pop_sent_msg().await;
@@ -149,7 +149,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
);
if case == SetupContactCase::SecurejoinWaitTimeout {
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT));
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false);
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
}
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
@@ -208,7 +208,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
.gossiped_keys
.insert(alice_addr.to_string(), wrong_pubkey)
.unwrap();
let contact_bob = alice.add_or_lookup_email_contact(&bob).await;
let contact_bob = alice.add_or_lookup_contact(&bob).await;
let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id)
.await
.unwrap();
@@ -318,7 +318,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
.check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT)
.await
.unwrap(),
(true, 0)
0
);
}
@@ -336,7 +336,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
assert!(msg.is_info());
assert_eq!(
msg.get_text(),
stock_str::securejoin_takes_longer(&bob).await
stock_str::securejoin_wait_timeout(&bob).await
);
}
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
@@ -715,7 +715,7 @@ async fn test_unknown_sender() -> Result<()> {
let sent = bob.send_text(bob_chat_id, "Hi hi!").await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
alice.pop_sent_msg().await;

View File

@@ -90,14 +90,13 @@ impl Smtp {
let lp = ConfiguredLoginParam::load(context)
.await?
.context("Not configured")?;
let proxy_config = ProxyConfig::load(context).await?;
self.connect(
context,
&lp.smtp,
&lp.smtp_password,
&proxy_config,
&lp.proxy_config,
&lp.addr,
lp.strict_tls(proxy_config.is_some()),
lp.strict_tls(),
lp.oauth2,
)
.await
@@ -439,7 +438,6 @@ pub(crate) async fn send_msg_to_smtp(
None,
None,
None,
None,
)
.await?;
};

Some files were not shown because too many files have changed in this diff Show More