Compare commits

...

32 Commits

Author SHA1 Message Date
link2xt
3efd94914c chore(release): prepare for 1.158.0 2025-03-29 16:40:10 +00:00
link2xt
99a6756d28 test: online test for renaming the group multiple times 2025-03-29 15:22:43 +00:00
link2xt
3310315865 test: set chat name multiple times in a row 2025-03-29 15:22:43 +00:00
link2xt
a7729e3548 fix: move group name timestamp update up in create_send_msg_jobs()
Otherwise outdated timestamp is rendered into the message.
2025-03-29 15:22:43 +00:00
link2xt
dc2e4df286 test: use vCards to create contacts in more Rust tests 2025-03-29 15:22:43 +00:00
link2xt
386b91a9a7 feat: stop saving txt_raw
It is redundant now that we have HTML view for long messages
and is not updated when the message is edited.
2025-03-29 15:10:57 +00:00
Hocuri
d4847206cf refactor: Move proxy_config out of ConfiguredLoginParam (#6712)
We want to store ConfiguredLoginParam in the database as Json per-login,
but proxy_config should be global for all logins.
2025-03-29 14:04:40 +01:00
link2xt
7624a50cb1 fix: do not fail to send the message if some keys are missing 2025-03-29 00:02:48 +00:00
link2xt
568c044a90 feat: simplify e2ee decision logic
Removed remaining majority vote code.
2025-03-28 15:12:32 +00:00
Hocuri
a8f8d34c25 feat: understandable error message when accounts.lock can't be locked (#6695)
Targets https://github.com/chatmail/core/issues/6636

Right now the error message is:

> Error: Delta Chat is already running. To use Delta Chat, you must
first close the existing Delta Chat process, or restart your device.
> 
> (accounts.lock lock file is already locked)

other suggestions welcome!
2025-03-27 12:33:29 +00:00
l
a308766e47 docs: make the logo rusty 2025-03-25 17:31:49 +00:00
Hocuri
0df86b6308 fix: fixes for transport JsonRPC (#6680)
Follow-up to #6582

---------

Co-authored-by: adbenitez <asieldbenitez@gmail.com>
2025-03-25 17:47:27 +01:00
link2xt
e951a697ec test: use TestContextManager in more tests 2025-03-25 16:44:42 +00:00
link2xt
1ebaa2a718 feat(securejoin): do not create 1:1 chat on Alice's side until vc-request-with-auth
vc-request is an unencrypted message
that Bob sends when he does not have Alice's key.
It also does not contain
Bob's avatar and name,
so the contact has only the email address
at this point and it is too early
to show it.
2025-03-24 14:21:56 +00:00
link2xt
6cb6daaab2 fix: synchronize contact name changes 2025-03-23 22:34:57 +00:00
link2xt
d25fb4770c test: use vCards more in Python tests 2025-03-23 15:45:42 +00:00
link2xt
e4e738ec5f api(deltachat-rpc-client): accept Account as Account.create_contact() argument 2025-03-23 15:45:42 +00:00
link2xt
8a5a67d6f2 refactor: move mark_recipients_as_verified() call out of has_verified_encryption() 2025-03-21 14:11:05 +00:00
Hocuri
ee68b9c7ba refactor: Use chat_id.get_timestamp() instead of duplicating its code (#6691) 2025-03-21 15:06:30 +01:00
Hocuri
a51b2fa751 refactor: Use created_timestamp() instead of duplicating its code (#6692) 2025-03-21 15:06:06 +01:00
link2xt
4c4646e72c test: use add_or_lookup_email_contact in test_setup_contact_ex 2025-03-21 13:01:13 +00:00
link2xt
2ca866b644 test: use add_or_lookup_email_contact() in get_chat()
This avoids importing the key via vCard
as a side effect of looking for a chat.
2025-03-21 13:01:13 +00:00
link2xt
ed7dfd6b65 test: remove test_group_with_removed_message_id
The test is mostly testing that groups can be matched
even if Message-ID is replaced.
Delta Chat no longer places group ID into Message-ID
or References, so the test is not
testing anything other than the ability
to match groups based on References header.
2025-03-21 13:01:13 +00:00
link2xt
de79cd1583 test: use vCard in TestContext.add_or_lookup_contact() 2025-03-21 13:01:13 +00:00
holger krekel
0e84cfd8ad docs: reference chatmail in the README 2025-03-21 10:42:15 +00:00
Hocuri
8a9e60afc3 feat: Nicer configuration error (#6684) 2025-03-20 18:56:12 +00:00
link2xt
b5fa6553af api: add ContactId.set_name()
This API allows to explicitly set
a name of the contact
instead of trying to create a new contact
with the same address.

Not all contacts are identified
by the email address
and we are going to introduce
contacts identified by their keys.
2025-03-20 14:38:58 +00:00
link2xt
5280448cd3 refactor: factor out update_chat_names() 2025-03-20 14:38:58 +00:00
link2xt
891e166996 build(deltachat-rpc-client): move development dependencies from tox.ini to pyproject.toml 2025-03-20 14:26:18 +00:00
link2xt
df24532503 chore: update resolve-conf from 0.7.0 to 0.7.1 2025-03-20 12:32:11 +00:00
Simon Laux
b82fa19c6f api: rename parameter name in get_webxdc_href to info_msg_id to reduce confusion potential (#6681) 2025-03-19 20:35:42 +01:00
link2xt
8cb136ab9d refactor: do not convert SQL arguments to String unnecessarily 2025-03-19 15:40:23 +00:00
49 changed files with 955 additions and 719 deletions

View File

@@ -1,5 +1,54 @@
# Changelog
## [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
@@ -6051,3 +6100,4 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[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

47
Cargo.lock generated
View File

@@ -1266,7 +1266,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.157.3"
version = "1.158.0"
dependencies = [
"anyhow",
"async-broadcast",
@@ -1377,7 +1377,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.157.3"
version = "1.158.0"
dependencies = [
"anyhow",
"async-channel 2.3.1",
@@ -1400,7 +1400,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.157.3"
version = "1.158.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1416,7 +1416,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.157.3"
version = "1.158.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1445,7 +1445,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.157.3"
version = "1.158.0"
dependencies = [
"anyhow",
"deltachat",
@@ -2444,13 +2444,13 @@ dependencies = [
[[package]]
name = "hostname"
version = "0.3.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
dependencies = [
"cfg-if",
"libc",
"match_cfg",
"winapi",
"windows 0.52.0",
]
[[package]]
@@ -2819,7 +2819,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
dependencies = [
"byteorder-lite",
"quick-error 2.0.1",
"quick-error",
]
[[package]]
@@ -3372,12 +3372,6 @@ dependencies = [
"quoted_printable",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matchers"
version = "0.1.0"
@@ -4571,12 +4565,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-error"
version = "2.0.1"
@@ -4910,12 +4898,11 @@ dependencies = [
[[package]]
name = "resolv-conf"
version = "0.7.0"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
checksum = "48375394603e3dd4b2d64371f7148fd8c7baa2680e28741f2cb8d23b59e3d4c4"
dependencies = [
"hostname",
"quick-error 1.2.3",
]
[[package]]
@@ -6736,6 +6723,16 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core 0.52.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.58.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.157.3"
version = "1.158.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.81"

View File

@@ -1,5 +1,5 @@
<p align="center">
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
<img alt="Chatmail logo" src="https://github.com/user-attachments/assets/25742da7-a837-48cd-a503-b303af55f10d" width="300" style="float:middle;" />
</p>
<p align="center">
@@ -11,9 +11,31 @@
</a>
</p>
<p align="center">
The core library for Delta Chat, written in Rust
</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).
## Installing Rust and Cargo

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.157.3"
version = "1.158.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

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

View File

@@ -457,7 +457,7 @@ impl CommandApi {
///
/// This function stops and starts IO as needed.
///
/// Usually it will be enough to only set `addr` and `imap.password`,
/// 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;
@@ -1537,6 +1537,7 @@ impl CommandApi {
Ok(())
}
/// Sets display name for existing contact.
async fn change_contact_name(
&self,
account_id: u32,
@@ -1545,9 +1546,7 @@ impl CommandApi {
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
let contact = Contact::get_by_id(&ctx, contact_id).await?;
let addr = contact.get_addr();
Contact::create(&ctx, &name, addr).await?;
contact_id.set_name(&ctx, &name).await?;
Ok(())
}
@@ -1947,13 +1946,9 @@ 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,
instance_msg_id: u32,
) -> Result<Option<String>> {
async fn get_webxdc_href(&self, account_id: u32, info_msg_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
let message = Message::load_from_db(&ctx, MsgId::new(info_msg_id)).await?;
Ok(message.get_webxdc_href())
}

View File

@@ -4,53 +4,6 @@ use serde::Deserialize;
use serde::Serialize;
use yerpc::TypeDef;
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct EnteredServerLoginParam {
/// Server hostname or IP address.
pub server: String,
/// Server port.
///
/// 0 if not specified.
pub port: u16,
/// Socket security.
pub security: Socket,
/// Username.
///
/// Empty string if not specified.
pub user: String,
/// Password.
pub password: String,
}
impl From<dc::EnteredServerLoginParam> for EnteredServerLoginParam {
fn from(param: dc::EnteredServerLoginParam) -> Self {
Self {
server: param.server,
port: param.port,
security: param.security.into(),
user: param.user,
password: param.password,
}
}
}
impl From<EnteredServerLoginParam> for dc::EnteredServerLoginParam {
fn from(param: EnteredServerLoginParam) -> Self {
Self {
server: param.server,
port: param.port,
security: param.security.into(),
user: param.user,
password: param.password,
}
}
}
/// Login parameters entered by the user.
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
@@ -59,28 +12,67 @@ pub struct EnteredLoginParam {
/// Email address.
pub addr: String,
/// IMAP settings.
pub imap: EnteredServerLoginParam,
/// Password.
pub password: String,
/// SMTP settings.
pub smtp: EnteredServerLoginParam,
/// 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
pub certificate_checks: EnteredCertificateChecks,
/// invalid hostnames.
/// Default: Automatic
pub certificate_checks: Option<EnteredCertificateChecks>,
/// If true, login via OAUTH2 (not recommended anymore)
pub oauth2: bool,
/// 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,
imap: param.imap.into(),
smtp: param.smtp.into(),
certificate_checks: param.certificate_checks.into(),
oauth2: param.oauth2,
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(),
}
}
}
@@ -91,18 +83,31 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
fn try_from(param: EnteredLoginParam) -> Result<Self> {
Ok(Self {
addr: param.addr,
imap: param.imap.into(),
smtp: param.smtp.into(),
certificate_checks: param.certificate_checks.into(),
oauth2: param.oauth2,
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)]
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum Socket {
/// Unspecified socket security, select automatically.
#[default]
Automatic,
/// TLS connection.
@@ -137,12 +142,13 @@ impl From<Socket> for dc::Socket {
}
}
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[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.
@@ -177,3 +183,19 @@ impl From<EnteredCertificateChecks> for dc::EnteredCertificateChecks {
}
}
}
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

@@ -673,7 +673,6 @@ 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>,
@@ -686,7 +685,6 @@ 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,
@@ -699,7 +697,6 @@ impl MessageInfo {
let hop_info = msg_id.hop_info(context).await?;
Ok(Self {
rawtext,
ephemeral_timer,
ephemeral_timestamp,
error: message.error(),

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.157.3"
version = "1.158.0"
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.157.3"
version = "1.158.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -70,3 +70,11 @@ line-length = 120
[tool.isort]
profile = "black"
[dependency-groups]
dev = [
"imap-tools",
"pytest",
"pytest-timeout",
"pytest-xdist",
]

View File

@@ -115,7 +115,7 @@ class Account:
self.start_io()
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
Calling this method will always result in the same
@@ -123,9 +123,15 @@ class Account:
with that e-mail address, it is unblocked and its display
name is updated if specified.
:param obj: email-address or contact id.
:param obj: email-address, contact id or account.
: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):
@@ -146,9 +152,8 @@ class Account:
return [Contact(self, contact_id) for contact_id in contact_ids]
def create_chat(self, account: "Account") -> Chat:
vcard = account.self_contact.make_vcard()
[contact] = self.import_vcard(vcard)
return contact.create_chat()
"""Create a 1:1 chat with another account."""
return self.create_contact(account).create_chat()
def get_device_chat(self) -> Chat:
"""Return device chat."""

View File

@@ -12,14 +12,6 @@ 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
@@ -32,26 +24,25 @@ class ACFactory:
def get_unconfigured_bot(self) -> Bot:
return Bot(self.get_unconfigured_account())
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
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}"
@futuremethod
def new_configured_account(self):
account = self.new_preconfigured_account()
yield account.configure.future()
addr, password = self.get_credentials()
account = self.get_unconfigured_account()
params = {"addr": addr, "password": password}
yield account._rpc.add_transport.future(account.id, params)
assert account.is_configured()
return account
def new_configured_bot(self) -> Bot:
credentials = get_temp_credentials()
addr, password = self.get_credentials()
bot = self.get_unconfigured_bot()
bot.configure(credentials["email"], credentials["password"])
bot.configure(addr, password)
return bot
@futuremethod

View File

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

View File

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

View File

@@ -117,8 +117,7 @@ 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)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -155,11 +154,8 @@ def test_qr_readreceipt(acfactory) -> None:
logging.info("Alice creates a verified group")
group = alice.create_group("Group", protect=True)
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")
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
group.add_contact(alice_contact_bob)
group.add_contact(alice_contact_charlie)
@@ -186,7 +182,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_addr, "Charlie")
bob_contact_charlie = bob.create_contact(charlie, "Charlie")
assert not bob.get_chat_by_contact(bob_contact_charlie)
logging.info("Charlie reads Bob's message")
@@ -462,8 +458,7 @@ 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)
# ac1new is only used to get a new address.
ac1new = acfactory.new_preconfigured_account()
addr, password = acfactory.get_credentials()
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
@@ -483,8 +478,8 @@ def test_aeap_flow_verified(acfactory):
assert msg_in_1.text == msg_out.text
logging.info("changing email account")
ac1.set_config("addr", ac1new.get_config("addr"))
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
ac1.set_config("addr", addr)
ac1.set_config("mail_pw", password)
ac1.stop_io()
ac1.configure()
ac1.start_io()
@@ -497,11 +492,9 @@ 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 == ac1new.get_config("addr")
assert msg_in_2.get_sender_contact().get_snapshot().address == addr
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
assert ac1new.get_config("addr") in [
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
]
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
def test_gossip_verification(acfactory) -> None:
@@ -517,9 +510,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.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")
bob_contact_alice = bob.create_contact(alice, "Alice")
bob_contact_carol = bob.create_contact(carol, "Carol")
carol_contact_alice = carol.create_contact(alice, "Alice")
logging.info("Bob creates an Autocrypt group")
bob_group_chat = bob.create_group("Autocrypt Group")
@@ -579,7 +572,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.get_config("addr"), "")
ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert ac2_contact_ac1.get_snapshot().is_verified
# ac1 resetups the account.
@@ -594,7 +587,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.get_config("addr"), "")
ac1_contact_ac2 = ac1.create_contact(ac2, "")
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
ac1_chat_ac2.send_text("Hello!")

View File

@@ -61,52 +61,77 @@ def test_acfactory(acfactory) -> None:
def test_configure_starttls(acfactory) -> None:
account = acfactory.new_preconfigured_account()
# Use STARTTLS
account.set_config("mail_security", "2")
account.set_config("send_security", "2")
account.configure()
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account._rpc.add_transport(
account.id,
{
"addr": addr,
"password": password,
"imapSecurity": "starttls",
"smtpSecurity": "starttls",
},
)
assert account.is_configured()
def test_configure_ip(acfactory) -> None:
account = acfactory.new_preconfigured_account()
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
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.configure()
account._rpc.add_transport(
account.id,
{
"addr": addr,
"password": password,
# This should fail TLS check.
"imapServer": ip_address,
},
)
def test_configure_alternative_port(acfactory) -> None:
"""Test that configuration with alternative port 443 works."""
account = acfactory.new_preconfigured_account()
account.set_config("mail_port", "443")
account.set_config("send_port", "443")
account.configure()
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account._rpc.add_transport(
account.id,
{
"addr": addr,
"password": password,
"imapPort": 443,
"smtpPort": 443,
},
)
assert account.is_configured()
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_list_transports(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account._rpc.add_transport(
account.id,
{
"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_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -171,8 +196,7 @@ def test_account(acfactory) -> None:
def test_chat(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -238,7 +262,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_addr, "Bob")
alice_contact_bob = alice.create_contact(bob, "Bob")
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
assert repr(alice_contact_bob)
@@ -255,8 +279,7 @@ def test_contact(acfactory) -> None:
def test_message(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -314,8 +337,7 @@ def test_reaction_seen_on_another_dev(acfactory) -> None:
alice2 = alice.clone()
alice2.start_io()
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
@@ -332,8 +354,7 @@ def test_reaction_seen_on_another_dev(acfactory) -> None:
alice2.clear_all_events()
alice_chat_bob.mark_noticed()
chat_id = alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id
alice2_contact_bob = alice2.get_contact_by_addr(bob_addr)
alice2_chat_bob = alice2_contact_bob.create_chat()
alice2_chat_bob = alice2.create_chat(bob)
assert chat_id == alice2_chat_bob.id
@@ -341,8 +362,7 @@ def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
# Alice becomes a bot.
@@ -401,9 +421,11 @@ 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.
bot = acfactory.new_preconfigured_account()
addr, password = acfactory.get_credentials()
bot = acfactory.get_unconfigured_account()
bot.set_config("bot", "1")
bot.configure()
bot._rpc.add_transport(bot.id, {"addr": addr, "password": password})
assert bot.is_configured()
# There are no old messages and the call returns immediately.
assert not bot.wait_next_messages()
@@ -412,8 +434,7 @@ def test_wait_next_messages(acfactory) -> None:
# Bot starts waiting for messages.
next_messages_task = executor.submit(bot.wait_next_messages)
bot_addr = bot.get_config("addr")
alice_contact_bot = alice.create_contact(bot_addr, "Bot")
alice_contact_bot = alice.create_contact(bot, "Bot")
alice_chat_bot = alice_contact_bot.create_chat()
alice_chat_bot.send_text("Hello!")
@@ -437,9 +458,7 @@ 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)
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 = alice.create_chat(bob)
alice_chat_bob.send_text("Hello Bob!")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
@@ -489,9 +508,7 @@ def test_provider_info(rpc) -> None:
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_bob = alice.create_contact(bob, "Bob")
# Bob creates chat manually so chat with Alice is accepted.
alice_chat_bob = alice_contact_bob.create_chat()
@@ -587,9 +604,13 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
ac2 = acfactory.new_preconfigured_account()
ac2.configure()
addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account()
ac2._rpc.add_transport(ac2.id, {"addr": addr, "password": password})
ac2.set_config("mvbox_move", "1")
assert ac2.is_configured()
ac2.bring_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
@@ -633,9 +654,7 @@ 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_addr = account.get_config("addr")
contact = alice.create_contact(contact_addr, "")
contact = alice.create_contact(account)
alice_group.add_contact(contact)
if n_accounts == 2:
@@ -742,3 +761,39 @@ 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

View File

@@ -12,11 +12,8 @@ setenv =
RUST_MIN_STACK=8388608
passenv =
CHATMAIL_DOMAIN
deps =
pytest
pytest-timeout
pytest-xdist
imap-tools
dependency_groups =
dev
[testenv:lint]
skipsdist = True

View File

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

View File

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

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!("Fatal error: {error:#}.")
log::error!("Error: {error:#}.")
}
std::process::exit(if r.is_ok() { 0 } else { 1 });
}

View File

@@ -37,7 +37,6 @@ skip = [
{ 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" },

View File

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

View File

@@ -1 +1 @@
2025-03-19
2025-03-29

View File

@@ -4,7 +4,7 @@ use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
use anyhow::{bail, ensure, Context as _, Result};
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
@@ -73,9 +73,7 @@ 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
.context("failed to load accounts config")?;
let config = Config::from_file(config_file, writable).await?;
let events = Events::new();
let stockstrings = StockStrings::new();
let push_subscriber = PushSubscriber::new();
@@ -460,7 +458,9 @@ impl Config {
rx.await?;
Ok(())
});
locked_rx.await?;
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)");
};
Ok(Some(lock_task))
}

View File

@@ -1080,7 +1080,8 @@ impl ChatId {
.unwrap_or(0))
}
/// Returns timestamp of the latest message in the chat.
/// Returns timestamp of the latest message in the chat,
/// including hidden messages or a draft if there is one.
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
let timestamp = context
.sql
@@ -1989,13 +1990,7 @@ impl Chat {
if let Some(member_list_timestamp) = self.param.get_i64(Param::MemberListTimestamp) {
Ok(member_list_timestamp)
} else {
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)
Ok(self.id.created_timestamp(context).await?)
}
}
@@ -3009,6 +3004,12 @@ 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;
@@ -3054,11 +3055,6 @@ 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),
@@ -4420,7 +4416,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, txt_raw, \
let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, \
mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
let row_id = context
.sql
@@ -4614,17 +4610,7 @@ 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) = context
.sql
.query_get_value(
"SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=?
HAVING COUNT(*) > 0",
(chat_id,),
)
.await?
{
if let Some(last_msg_time) = chat_id.get_timestamp(context).await? {
if timestamp_sort <= last_msg_time {
timestamp_sort = last_msg_time + 1;
}

View File

@@ -298,9 +298,11 @@ 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 = Contact::create(&alice, "robert", "bob@example.net").await?;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
alice_bob_contact_id.set_name(&alice, "robert").await?;
// Set Bob authname to "Bob" and send it to Alice.
bob.set_config(Config::Displayname, Some("Bob")).await?;
@@ -320,19 +322,15 @@ 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 = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
let sent = alice
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!(
@@ -343,9 +341,6 @@ async fn test_member_add_remove() -> Result<()> {
// 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(),
@@ -355,7 +350,6 @@ async fn test_member_add_remove() -> Result<()> {
// 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(())
@@ -368,10 +362,12 @@ 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 = 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?;
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;
// Create and promote a group.
let alice_chat_id =
@@ -386,8 +382,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 Claire to the chat.
add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
// Alice adds Charlie to the chat.
add_contact_to_chat(&alice, alice_chat_id, alice_charlie_contact_id).await?;
let alice_sent_add_msg = alice.pop_sent_msg().await;
// Bob leaves the chat.
@@ -674,12 +670,13 @@ async fn test_modify_chat_lost() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_leave_group() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Create group chat with Bob.
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
let bob_contact = Contact::create(&alice, "", "bob@example.net").await?;
let bob_contact = alice.add_or_lookup_contact(&bob).await.id;
add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?;
// Alice sends first message to group.
@@ -1392,20 +1389,52 @@ async fn test_pinned_after_new_msgs() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_chat_name() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
assert_eq!(
Chat::load_from_db(&t, chat_id).await.unwrap().get_name(),
Chat::load_from_db(alice, chat_id).await.unwrap().get_name(),
"foo"
);
set_chat_name(&t, chat_id, "bar").await.unwrap();
set_chat_name(alice, chat_id, "bar").await.unwrap();
assert_eq!(
Chat::load_from_db(&t, chat_id).await.unwrap().get_name(),
Chat::load_from_db(alice, 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)]
@@ -1615,58 +1644,6 @@ 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;
@@ -2091,8 +2068,9 @@ async fn test_forward_quote() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_group() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
@@ -2314,11 +2292,13 @@ 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,
Contact::create(&alice, "", "bob@example.net").await?,
alice.add_or_lookup_contact_id(&bob).await,
)
.await?;
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
@@ -2327,7 +2307,7 @@ async fn test_resend_own_message() -> Result<()> {
add_contact_to_chat(
&alice,
alice_grp,
Contact::create(&alice, "", "claire@example.org").await?,
alice.add_or_lookup_contact_id(&fiona).await,
)
.await?;
let sent2 = alice.pop_sent_msg().await;
@@ -2371,15 +2351,13 @@ 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);
// 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;
// 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;
assert_eq!(msg.get_text(), "alice->bob");
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!(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!(msg_from.get_addr(), "alice@example.org");
assert!(sent1_ts_sent < msg.timestamp_sent);
@@ -2863,12 +2841,7 @@ 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,
Contact::create(&alice, "bob", "bob@example.net").await?,
)
.await?;
add_contact_to_chat(&alice, chat_id, alice.add_or_lookup_contact_id(&bob).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);
@@ -2900,7 +2873,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(&bob).await.id;
let a0b_contact_id = alice0.add_or_lookup_contact_id(&bob).await;
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(alice0).await?;
@@ -2955,12 +2928,13 @@ async fn test_sync_blocked() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_accept_before_first_msg() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let bob = TestContext::new_bob().await;
let bob = &tcm.bob().await;
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
@@ -2974,7 +2948,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?;
@@ -2983,7 +2957,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);
@@ -3119,8 +3093,9 @@ 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 alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
@@ -3172,8 +3147,9 @@ async fn test_sync_device_messages_visibility() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_muted() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
@@ -3207,8 +3183,9 @@ async fn test_sync_muted() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_broadcast() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
let alice1 = &TestContext::new_alice().await;
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
@@ -3397,7 +3374,8 @@ async fn test_past_members() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
let fiona = &tcm.fiona().await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3409,8 +3387,7 @@ 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 bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let add_message = alice.pop_sent_msg().await;
@@ -3429,8 +3406,7 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.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_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3442,7 +3418,8 @@ 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 bob_fiona_contact_id = Contact::create(bob, "Fiona", "fiona@example.net").await?;
let fiona = &tcm.fiona().await;
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).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?;
@@ -3464,11 +3441,10 @@ async fn unpromoted_group_no_tombstones() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.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_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_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3484,10 +3460,6 @@ 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;
@@ -3503,8 +3475,8 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let fiona_addr = "fiona@example.net";
let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?;
let fiona = &tcm.fiona().await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3519,12 +3491,10 @@ 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 bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).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);
@@ -3598,13 +3568,11 @@ 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 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_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 alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
@@ -3626,7 +3594,6 @@ 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.
@@ -3635,8 +3602,7 @@ 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 fiona_addr = fiona.get_config(Config::Addr).await?.unwrap();
let bob_fiona_contact_id = Contact::create(bob, "Fiona", &fiona_addr).await?;
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await;
add_contact_to_chat(bob, bob_chat_id, bob_fiona_contact_id).await?;
let add_message = bob.pop_sent_msg().await;
@@ -3700,7 +3666,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 = Contact::create(bob, "Charlie", charlie_addr).await?;
let bob_charlie_contact_id = bob.add_or_lookup_contact_id(charlie).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);

View File

@@ -144,7 +144,8 @@ impl Context {
// 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));
progress!(self, 0, Some(error_msg.clone()));
bail!(error_msg);
} else {
param.save(self).await?;
progress!(self, 1000);
@@ -157,9 +158,22 @@ impl Context {
/// using the server encoded in the QR code.
/// See [Self::add_transport].
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
set_account_from_qr(self, qr).await?;
self.configure().await?;
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(())
}
@@ -418,7 +432,6 @@ async fn get_configured_param(
.collect(),
smtp_user: param.smtp.user.clone(),
smtp_password,
proxy_config: ProxyConfig::load(ctx).await?,
provider,
certificate_checks: match param.certificate_checks {
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
@@ -440,7 +453,8 @@ 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 strict_tls = configured_param.strict_tls();
let proxy_config = ProxyConfig::load(ctx).await?;
let strict_tls = configured_param.strict_tls(proxy_config.is_some());
progress!(ctx, 550);
@@ -450,15 +464,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_config,
&proxy_config2,
&smtp_addr,
strict_tls,
configured_param.oauth2,
@@ -476,7 +490,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
let mut imap = Imap::new(
configured_param.imap.clone(),
configured_param.imap_password.clone(),
configured_param.proxy_config.clone(),
proxy_config,
&configured_param.addr,
strict_tls,
configured_param.oauth2,
@@ -485,7 +499,10 @@ 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, err.to_string()).await),
Err(err) => bail!(
"{}",
nicer_configuration_error(ctx, format!("{err:#}")).await
),
};
progress!(ctx, 850);

View File

@@ -95,6 +95,50 @@ 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
@@ -843,44 +887,48 @@ 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.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)?;
(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)?;
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 = 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
};
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
};
transaction
.execute(
transaction.execute(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
(
new_name,
@@ -899,88 +947,38 @@ impl Contact {
} else {
row_authname
},
row_id
row_id,
),
)?;
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()?;
if let Some(chat_id) = chat_id {
if update_name || update_authname {
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);
}
update_chat_names(context, transaction, contact_id)?;
}
sth_modified = Modifier::Modified;
}
sth_modified = Modifier::Modified;
}
} else {
let update_name = manual;
let update_authname = !manual;
} else {
let update_name = manual;
let update_authname = !manual;
transaction
.execute(
transaction.execute(
"INSERT INTO contacts (name, addr, origin, authname)
VALUES (?, ?, ?, ?);",
(
if update_name {
name.to_string()
} else {
"".to_string()
},
(
if update_name { &name } else { "" },
&addr,
origin,
if update_authname {
name.to_string()
} else {
"".to_string()
}
if update_authname { &name } else { "" },
),
)?;
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);
@@ -1606,6 +1604,60 @@ 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,

View File

@@ -1050,8 +1050,9 @@ async fn test_sync_create() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_make_n_import_vcard() -> Result<()> {
let alice = &TestContext::new_alice().await;
let bob = &TestContext::new_bob().await;
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.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

@@ -1,8 +1,9 @@
//! End-to-end encryption support.
use std::collections::BTreeSet;
use std::io::Cursor;
use anyhow::{format_err, Context as _, Result};
use anyhow::{bail, Result};
use mail_builder::mime::MimePart;
use num_traits::FromPrimitive;
@@ -42,79 +43,76 @@ 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?;
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);
}
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;
}
}
return Ok(false);
}
// 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)
Ok(true)
}
/// Tries to encrypt the passed in `mail`.
pub async fn encrypt(
self,
/// 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,
context: &Context,
verified: bool,
mail_to_encrypt: MimePart<'static>,
peerstates: Vec<(Option<Peerstate>, String)>,
compress: bool,
) -> Result<String> {
let mut keyring: Vec<SignedPublicKey> = Vec::new();
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));
}
let mut verifier_addresses: Vec<&str> = Vec::new();
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);
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());
}
}
// Encrypt to self.
keyring.push(self.public_key.clone());
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 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(),
@@ -128,6 +126,17 @@ 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();
@@ -324,22 +333,17 @@ 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, true, &ps).await?);
// Own preference is `Mutual` and we have the peer's key.
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
let ps = new_peerstates(EncryptPreference::Reset);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
let ps = new_peerstates(EncryptPreference::Mutual);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
// test with missing peerstate
let ps = vec![(None, "bob@foo.bar".to_string())];
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await.is_err());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
Ok(())
}

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;
use crate::test_utils::{TestContext, TestContextManager};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_htmlparse_plain_unspecified() {
@@ -442,24 +442,25 @@ 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 alice = TestContext::new_alice().await;
let mut tcm = TestContextManager::new();
let alice = &tcm.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;
@@ -468,11 +469,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 = TestContext::new_bob().await;
let bob = &tcm.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);
@@ -481,7 +482,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>"));
}
@@ -519,10 +520,11 @@ 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 = TestContext::new_alice().await;
let alice = &tcm.alice().await;
alice
.set_config(Config::ShowEmails, Some("1"))
.await
@@ -531,19 +533,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 = TestContext::new_alice().await;
let alice = &tcm.alice().await;
alice
.set_config(Config::ShowEmails, Some("0"))
.await
@@ -556,38 +558,39 @@ 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 alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.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,12 +271,14 @@ 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(),
param.proxy_config.clone(),
proxy_config,
&param.addr,
param.strict_tls(),
strict_tls,
param.oauth2,
idle_interrupt_receiver,
);
@@ -348,10 +350,11 @@ impl Imap {
connection_candidate,
)
.await
.context("IMAP failed to connect")
{
Ok(client) => client,
Err(err) => {
warn!(context, "IMAP failed to connect: {err:#}.");
warn!(context, "{err:#}.");
first_error.get_or_insert(err);
continue;
}

View File

@@ -441,9 +441,6 @@ 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
@@ -742,8 +739,6 @@ impl ConfiguredLoginParam {
}];
}
let proxy_config = ProxyConfig::load(context).await?;
Ok(Some(ConfiguredLoginParam {
addr,
imap,
@@ -754,7 +749,6 @@ impl ConfiguredLoginParam {
smtp_password: send_pw,
certificate_checks,
provider,
proxy_config,
oauth2,
}))
}
@@ -837,11 +831,11 @@ impl ConfiguredLoginParam {
Ok(())
}
pub(crate) fn strict_tls(&self) -> bool {
pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool {
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
match self.certificate_checks {
ConfiguredCertificateChecks::OldAutomatic => {
provider_strict_tls.unwrap_or(self.proxy_config.is_some())
provider_strict_tls.unwrap_or(connected_through_proxy)
}
ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true),
ConfiguredCertificateChecks::Strict => true,
@@ -962,8 +956,6 @@ 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,
@@ -1066,7 +1058,6 @@ 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_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL,
};
use crate::contact::{self, Contact, ContactId};
use crate::context::Context;
@@ -35,7 +35,7 @@ use crate::summary::Summary;
use crate::sync::SyncData;
use crate::tools::{
buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
sanitize_filename, time, timestamp_to_str, truncate,
sanitize_filename, time, timestamp_to_str,
};
/// Message ID, including reserved IDs.
@@ -174,15 +174,6 @@ 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,
@@ -219,12 +210,9 @@ 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}");
@@ -335,9 +323,6 @@ 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);
@@ -1732,6 +1717,10 @@ 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());

View File

@@ -86,7 +86,9 @@ 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.
/// 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.
to: Vec<(String, String)>,
/// Vector of pairs of past group member names and addresses.
@@ -784,9 +786,7 @@ impl MimeFactory {
let peerstates = self.peerstates_for_recipients(context).await?;
let is_encrypted = !self.should_force_plaintext()
&& encrypt_helper
.should_encrypt(context, e2ee_guaranteed, &peerstates)
.await?;
&& (e2ee_guaranteed || encrypt_helper.should_encrypt(context, &peerstates).await?);
let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded {
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
} else {
@@ -982,14 +982,23 @@ 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, verified, message, peerstates, compress)
.encrypt(context, encryption_keyring, message, 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\"",

View File

@@ -731,8 +731,9 @@ Here's my footer -- bob@example.net"
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reaction_summary() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.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;

View File

@@ -405,8 +405,11 @@ pub(crate) async fn receive_imf_inner(
received_msg = None;
}
let verified_encryption =
has_verified_encryption(context, &mime_parser, from_id, &to_ids).await?;
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?;
}
if verified_encryption == VerifiedEncryption::Verified
&& mime_parser.get_header(HeaderDef::ChatVerified).is_some()
@@ -1536,7 +1539,6 @@ async fn add_parts(
}
}
let mut txt_raw = "".to_string();
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
if better_msg.is_empty() && is_partial_download.is_none() {
chat_id = DC_CHAT_ID_TRASH;
@@ -1551,11 +1553,6 @@ async fn add_parts(
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 {
@@ -1582,7 +1579,7 @@ INSERT INTO msgs
rfc724_mid, chat_id,
from_id, to_id, timestamp, timestamp_sent,
timestamp_rcvd, type, state, msgrmsg,
txt, txt_normalized, subject, txt_raw, param, hidden,
txt, txt_normalized, subject, param, hidden,
bytes, mime_headers, mime_compressed, mime_in_reply_to,
mime_references, mime_modified, error, ephemeral_timer,
ephemeral_timestamp, download_state, hop_info
@@ -1591,7 +1588,7 @@ INSERT INTO msgs
?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?, ?, 1,
?, ?, ?, ?,
?, ?, ?, ?
@@ -1601,7 +1598,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,
txt_raw=excluded.txt_raw, param=excluded.param,
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,
@@ -1623,8 +1620,6 @@ 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 {
@@ -3059,23 +3054,12 @@ 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.
///
/// Also propagates gossiped keys to verified if needed.
async fn has_verified_encryption(
context: &Context,
fn has_verified_encryption(
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()));
};
@@ -3104,21 +3088,24 @@ async 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: Vec<ContactId>,
to_ids: &[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 {
for &id in to_ids {
if id == ContactId::SELF {
continue;
}
let Some((to_addr, is_verified)) = context
.sql
.query_row_optional(

View File

@@ -4066,38 +4066,39 @@ 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 alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
let claire_id = Contact::create(&alice, "", "claire@example.de").await?;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let fiona_id = alice.add_or_lookup_contact_id(fiona).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?;
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?;
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, claire_id).await?;
remove_contact_from_chat(alice, alice_chat_id, fiona_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(())
}
@@ -4109,8 +4110,7 @@ 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 =
Contact::create(alice, "", &bob.get_config(Config::Addr).await?.unwrap()).await?;
let alice_bob_id = alice.add_or_lookup_contact_id(bob).await;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
// Alice creates a group chat. Bob accepts it.
@@ -4798,13 +4798,14 @@ 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_id = Contact::create(&alice, "", "bob@example.net").await?;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
let bob_id = alice.add_or_lookup_contact_id(&bob).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?;
@@ -4839,7 +4840,7 @@ Chat-Group-Member-Added: charlie@example.com",
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "fiona", "fiona@example.net").await?,
alice.add_or_lookup_contact_id(&fiona).await,
)
.await?;
@@ -4924,11 +4925,10 @@ 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;
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.
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!(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());
@@ -4937,10 +4937,6 @@ 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;
@@ -4949,7 +4945,6 @@ 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(())
}
@@ -5416,14 +5411,16 @@ Hello!"
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_rename_chat_on_missing_message() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let charlie = tcm.charlie().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_to_chat_contacts_table(
&alice,
time(),
chat_id,
&[Contact::create(&alice, "bob", "bob@example.net").await?],
&[alice.add_or_lookup_contact_id(&bob).await],
)
.await?;
send_text_msg(&alice, chat_id, "populate".to_string()).await?;
@@ -5437,8 +5434,8 @@ async fn test_rename_chat_on_missing_message() -> Result<()> {
bob.pop_sent_msg().await;
// Bob adds a new member.
let bob_orange = Contact::create(&bob, "orange", "orange@example.net").await?;
add_contact_to_chat(&bob, bob_chat_id, bob_orange).await?;
let bob_charlie = bob.add_or_lookup_contact_id(&charlie).await;
add_contact_to_chat(&bob, bob_chat_id, bob_charlie).await?;
let add_msg = bob.pop_sent_msg().await;
// Alice only receives the member addition.

View File

@@ -334,12 +334,6 @@ 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,
@@ -435,6 +429,11 @@ 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

@@ -129,7 +129,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
.await
.unwrap()
.len(),
1
0
);
let sent = alice.pop_sent_msg().await;
@@ -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_contact(&bob).await;
let contact_bob = alice.add_or_lookup_email_contact(&bob).await;
let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id)
.await
.unwrap();

View File

@@ -90,13 +90,14 @@ 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,
&lp.proxy_config,
&proxy_config,
&lp.addr,
lp.strict_tls(),
lp.strict_tls(proxy_config.is_some()),
lp.oauth2,
)
.await

View File

@@ -66,7 +66,7 @@ CREATE TABLE msgs (
msgrmsg INTEGER DEFAULT 1,
bytes INTEGER DEFAULT 0,
txt TEXT DEFAULT '',
txt_raw TEXT DEFAULT '',
txt_raw TEXT DEFAULT '', -- deprecated 2025-03-29
param TEXT DEFAULT '',
starred INTEGER DEFAULT 0,
timestamp_sent INTEGER DEFAULT 0,

View File

@@ -81,6 +81,14 @@ impl TestContextManager {
.await
}
pub async fn charlie(&mut self) -> TestContext {
TestContext::builder()
.configure_charlie()
.with_log_sink(self.log_sink.clone())
.build()
.await
}
pub async fn fiona(&mut self) -> TestContext {
TestContext::builder()
.configure_fiona()
@@ -239,6 +247,13 @@ impl TestContextBuilder {
self.with_key_pair(bob_keypair())
}
/// Configures as charlie@example.net with fixed secret key.
///
/// This is a shortcut for `.with_key_pair(fiona_keypair())`.
pub fn configure_charlie(self) -> Self {
self.with_key_pair(charlie_keypair())
}
/// Configures as fiona@example.net with fixed secret key.
///
/// This is a shortcut for `.with_key_pair(fiona_keypair())`.
@@ -682,7 +697,7 @@ impl TestContext {
}
/// Returns the [`ContactId`] for the other [`TestContext`], creating a contact if necessary.
pub async fn add_or_lookup_contact_id(&self, other: &TestContext) -> ContactId {
pub async fn add_or_lookup_email_contact_id(&self, other: &TestContext) -> ContactId {
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
let addr = ContactAddress::new(&primary_self_addr).unwrap();
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
@@ -699,6 +714,20 @@ impl TestContext {
contact_id
}
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
pub async fn add_or_lookup_email_contact(&self, other: &TestContext) -> Contact {
let contact_id = self.add_or_lookup_email_contact_id(other).await;
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
}
/// Returns the [`ContactId`] for the other [`TestContext`], creating it if necessary.
pub async fn add_or_lookup_contact_id(&self, other: &TestContext) -> ContactId {
let vcard = make_vcard(other, &[ContactId::SELF]).await.unwrap();
let contact_ids = import_vcard(self, &vcard).await.unwrap();
assert_eq!(contact_ids.len(), 1);
*contact_ids.first().unwrap()
}
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact {
let contact_id = self.add_or_lookup_contact_id(other).await;
@@ -711,7 +740,7 @@ impl TestContext {
/// This first creates a contact using the configured details on the other account, then
/// gets the 1:1 chat with this contact.
pub async fn get_chat(&self, other: &TestContext) -> Chat {
let contact = self.add_or_lookup_contact(other).await;
let contact = self.add_or_lookup_email_contact(other).await;
let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact.id)
.await
@@ -744,7 +773,7 @@ impl TestContext {
///
/// This function can be used to create unencrypted chats.
pub async fn create_email_chat(&self, other: &TestContext) -> Chat {
let contact = self.add_or_lookup_contact(other).await;
let contact = self.add_or_lookup_email_contact(other).await;
let chat_id = ChatId::create_for_contact(self, contact.id).await.unwrap();
Chat::load_from_db(self, chat_id).await.unwrap()
@@ -1104,6 +1133,21 @@ pub fn bob_keypair() -> KeyPair {
KeyPair { public, secret }
}
/// Load a pre-generated keypair for charlie@example.net from disk.
///
/// Like [alice_keypair] but a different key and identity.
pub fn charlie_keypair() -> KeyPair {
let public =
key::SignedPublicKey::from_asc(include_str!("../test-data/key/charlie-public.asc"))
.unwrap()
.0;
let secret =
key::SignedSecretKey::from_asc(include_str!("../test-data/key/charlie-secret.asc"))
.unwrap()
.0;
KeyPair { public, secret }
}
/// Load a pre-generated keypair for fiona@example.net from disk.
///
/// Like [alice_keypair] but a different key and identity.

View File

@@ -1,7 +1,9 @@
use anyhow::Result;
use pretty_assertions::assert_eq;
use crate::chat::{self, add_contact_to_chat, Chat, ProtectionStatus};
use crate::chat::{
self, add_contact_to_chat, remove_contact_from_chat, send_msg, Chat, ProtectionStatus,
};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::{Chattype, DC_GCL_FOR_FORWARDING};
@@ -953,6 +955,70 @@ async fn test_no_unencrypted_name_if_encrypted() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_lost_member_added() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
tcm.execute_securejoin(bob, alice).await;
tcm.execute_securejoin(fiona, alice).await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
.await;
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
assert_eq!(chat::get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
// Attempt to add member, but message is lost.
let fiona_id = alice.add_or_lookup_contact(fiona).await.id;
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
alice.pop_sent_msg().await;
let alice_sent = alice.send_text(alice_chat_id, "Hi again!").await;
bob.recv_msg(&alice_sent).await;
assert_eq!(chat::get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
bob_chat_id.accept(bob).await?;
let sent = bob.send_text(bob_chat_id, "Hello!").await;
let sent_msg = Message::load_from_db(bob, sent.sender_msg_id).await?;
assert_eq!(sent_msg.get_showpadlock(), true);
// The message will not be sent to Fiona.
// Test that Fiona will not be able to decrypt it.
let fiona_rcvd = fiona.recv_msg(&sent).await;
assert_eq!(fiona_rcvd.get_showpadlock(), false);
assert_eq!(
fiona_rcvd.get_text(),
"[...] [This message was encrypted for another setup.]"
);
// Advance the time so Alice does not leave at the same second
// as the group was created.
SystemTime::shift(std::time::Duration::from_secs(100));
// Alice leaves the chat.
remove_contact_from_chat(alice, alice_chat_id, ContactId::SELF).await?;
assert_eq!(
chat::get_chat_contacts(alice, alice_chat_id).await?.len(),
2
);
bob.recv_msg(&alice.pop_sent_msg().await).await;
// Now only Bob and Fiona are in the chat.
assert_eq!(chat::get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
// Bob cannot send messages anymore because there are no recipients
// other than self for which Bob has the key.
let mut msg = Message::new_text("No key for Fiona".to_string());
let result = send_msg(bob, bob_chat_id, &mut msg).await;
assert!(result.is_err());
Ok(())
}
// ============== Helper Functions ==============
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {

View File

@@ -43,8 +43,6 @@ async fn test_parse_receive_headers_integration() {
let raw = include_bytes!("../../test-data/message/mail_with_cc.txt");
let expected = r"State: Fresh
hi
Message-ID: 2dfdbde7@example.org
Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000
@@ -56,13 +54,6 @@ DKIM Results: Passed=true";
let raw = include_bytes!("../../test-data/message/encrypted_with_received_headers.eml");
let expected = "State: Fresh, Encrypted
Re: Message from alice@example.org
hi back\r\n\
\r\n\
-- \r\n\
Sent with my Delta Chat Messenger: https://delta.chat
Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net
Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000

View File

@@ -192,9 +192,10 @@ async fn test_forward_webxdc_instance() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resend_webxdc_instance_and_info() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Alice uses webxdc in a group
let alice = tcm.alice().await;
alice.set_config_bool(Config::BccSelf, false).await?;
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
let alice_instance = send_webxdc_instance(&alice, alice_grp).await?;
@@ -212,7 +213,7 @@ async fn test_resend_webxdc_instance_and_info() -> Result<()> {
add_contact_to_chat(
&alice,
alice_grp,
Contact::create(&alice, "", "bob@example.net").await?,
alice.add_or_lookup_contact_id(&bob).await,
)
.await?;
assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 3);
@@ -222,7 +223,6 @@ async fn test_resend_webxdc_instance_and_info() -> Result<()> {
let sent2 = alice.pop_sent_msg().await;
// Bob receives webxdc, legacy info-messages updates are received and added to the chat.
let bob = tcm.bob().await;
let bob_instance = bob.recv_msg(&sent1).await;
bob.recv_msg_trash(&sent2).await;
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);

View File

@@ -1,7 +1,7 @@
Group#Chat#10: Group chat [3 member(s)]
--------------------------------------------------------------------------------
Msg#10: (Contact#Contact#10): Hi! I created a group. [FRESH]
Msg#11: Me (Contact#Contact#Self): You left the group. [INFO] √
Msg#12: (Contact#Contact#10): Member claire@example.net added by alice@example.org. [FRESH][INFO]
Msg#13: (Contact#Contact#10): What a silence! [FRESH]
Msg#10🔒: (Contact#Contact#10): Hi! I created a group. [FRESH]
Msg#11🔒: Me (Contact#Contact#Self): You left the group. [INFO] √
Msg#12🔒: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO]
Msg#13🔒: (Contact#Contact#10): What a silence! [FRESH]
--------------------------------------------------------------------------------