Compare commits

...
Author SHA1 Message Date
Hocuri fba6e885ae More multidevice fixes; it's still not working completely 2025-07-21 17:32:54 +02:00
Hocuri eadbf41383 Fix multidevice 2025-07-12 13:45:35 +02:00
Hocuri 70ab41d7c2 WIP, untested: Receiving side of passing broadcast secret in a message 2025-07-12 10:48:33 +02:00
Hocuri fab39fdb97 WIP, untested: Sending side of transferring the secret in member-added message 2025-07-12 10:16:45 +02:00
Hocuri 63664396dd feat: Save the secret to encrypt and decrypt messages. Next: Send it in a 'member added' message. 2025-07-12 10:15:59 +02:00
Hocuri f10f65333b WIP: Start with decryption, and a test for it. Next TODO: SQL table migartion. 2025-07-12 10:15:59 +02:00
Hocuri 7cf42a43e7 feat: Symmetric encryption. No decryption, no sharing of the secret, not tested. 2025-07-12 10:15:56 +02:00
link2xt 402e42f858 chore(release): prepare for 2.1.0 2025-07-11 22:56:57 +00:00
link2xt fbae0739a6 chore(cargo): update cordyceps from 0.3.2 to 0.3.4 2025-07-11 22:07:27 +00:00
iequidooandl 0359481ba4 feat: ensure_and_debug_assert{,_eq,_ne} macros combining debug_assert* and anyhow::ensure (#6907)
We have some debug assertions already, but we also want the corresponding errors in the release
configuration so that it's not less reliable than non-optimized one. This doesn't change any
function signatures, only debug assertions in functions returning `Result` are replaced.

Co-authored-by: l <link2xt@testrun.org>
2025-07-11 14:59:49 -03:00
Hocuriandl 6406f305b8 feat: Make it possible to leave broadcast channels (#6984)
Part of #6884.
The channel owner will not be notified in any way that you left, they
will only see that there is one member less.

For the translated stock strings, this is what we agreed on in the
group:
- Add a new "Leave Channel" stock string (will need to be done in UIs)
- Reword the existing "Are you sure you want to leave this group?"
string to "Are you sure you want to leave?" (the options are "Cancel"
and "Leave Group" / "Leave Channel", so it's clear what you are leaving)
(will need to be done in the deltachat-android repo, other UIs will pick
it up automatically)
- Reword the existing "You left the group." string to "You left". (done
here, I will adapt the strings in deltachat-android, too)

I adapted DC Android by pushing
https://github.com/deltachat/deltachat-android/pull/3783/commits/6df2740884e0b781c1debd1783152fd9b2fe6787
to https://github.com/deltachat/deltachat-android/pull/3783.

---------

Co-authored-by: l <link2xt@testrun.org>
2025-07-11 12:34:05 +00:00
Hocuri e5e0f0cdd7 test: Add option to save database on test failure (#6992)
I had it a few times now that I wanted to examine the database in order
to debug a test failure. Setting this environment variable makes this
easy in the future.
2025-07-11 12:01:04 +00:00
B. Petersen 0bac4acdd8 docs: update showpadlock ffi 2025-07-11 13:11:01 +02:00
Alireza Sadraii ce5697c5f7 feat: add account ordering functionality (#6993)
New public API `set_accounts_order` allows setting the order of accounts.

The account order is stored as a list of account IDs in `accounts.toml`
under a new `accounts_order: Vec<u32>` field.
2025-07-10 22:59:27 +00:00
ivnandiequidoo 22258f7269 fix!: Use Viewtype::File for messages with invalid images, images of unknown size, images > 50 Mpx (#6825)
BREAKING CHANGE: messages with invalid images, images of unknown size,
huge images, will have Viewtype::File

After changing the logic of Viewtype selection, I had to fix 3 old tests
that used invalid Base64 image data.

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-07-10 18:11:55 -03:00
36 changed files with 908 additions and 213 deletions
+28
View File
@@ -1,5 +1,32 @@
# Changelog
## [2.1.0] - 2025-07-11
### Features / Changes
- Add account ordering functionality ([#6993](https://github.com/chatmail/core/pull/6993)).
- feat: Make it possible to leave broadcast channels ([#6984](https://github.com/chatmail/core/pull/6984))
- Migrations: Use tools::Time to measure time for logging.
- Log emitted logging events with `tracing`.
- Ensure_and_debug_assert{,_eq,_ne} macros combining `debug_assert*` and anyhow::ensure ([#6907](https://github.com/chatmail/core/pull/6907)).
### Fixes
- Use Viewtype::File for messages with invalid images, images of unknown size, images > 50 Mpx ([#6825](https://github.com/chatmail/core/pull/6825)).
- Don't apply chat name and avatar changes from non-members.
### Documentation
- Update showpadlock ffi.
### Miscellaneous Tasks
- cargo: Update cordyceps from 0.3.2 to 0.3.4.
### Tests
- Add option to save database on test failure ([#6992](https://github.com/chatmail/core/pull/6992)).
## [2.0.0] - 2025-07-09
This release changes the way the core handles contact keys.
@@ -6426,3 +6453,4 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[1.159.5]: https://github.com/chatmail/core/compare/v1.159.4..v1.159.5
[1.160.0]: https://github.com/chatmail/core/compare/v1.159.5..v1.160.0
[2.0.0]: https://github.com/chatmail/core/compare/v1.160.0..v2.0.0
[2.1.0]: https://github.com/chatmail/core/compare/v2.0.0..v2.1.0
Generated
+11 -46
View File
@@ -958,11 +958,11 @@ checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8"
[[package]]
name = "cordyceps"
version = "0.3.2"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec10f0a762d93c4498d2e97a333805cb6250d60bead623f71d8034f9a4152ba3"
checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a"
dependencies = [
"loom 0.5.6",
"loom",
"tracing",
]
@@ -1285,7 +1285,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.0.0"
version = "2.1.0"
dependencies = [
"anyhow",
"async-broadcast",
@@ -1395,7 +1395,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.0.0"
version = "2.1.0"
dependencies = [
"anyhow",
"async-channel 2.3.1",
@@ -1417,7 +1417,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.0.0"
version = "2.1.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1433,7 +1433,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.0.0"
version = "2.1.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1462,7 +1462,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.0.0"
version = "2.1.0"
dependencies = [
"anyhow",
"deltachat",
@@ -2205,19 +2205,6 @@ dependencies = [
"slab",
]
[[package]]
name = "generator"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"windows 0.48.0",
]
[[package]]
name = "generator"
version = "0.8.4"
@@ -3340,19 +3327,6 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "loom"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
dependencies = [
"cfg-if",
"generator 0.7.5",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "loom"
version = "0.7.2"
@@ -3360,7 +3334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator 0.8.4",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
@@ -3477,7 +3451,7 @@ dependencies = [
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"loom 0.7.2",
"loom",
"parking_lot",
"portable-atomic",
"rustc_version",
@@ -3852,7 +3826,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [
"proc-macro-crate 3.2.0",
"proc-macro-crate 2.0.0",
"proc-macro2",
"quote",
"syn 2.0.104",
@@ -6750,15 +6724,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows"
version = "0.52.0"
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.0.0"
version = "2.1.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.85"
@@ -15,7 +15,7 @@ opt-level = 1
# Make anyhow `backtrace` feature useful.
# With `debug = 0` there are no line numbers in the backtrace
# produced with RUST_BACKTRACE=1.
debug = 1
debug = 'full'
opt-level = 0
[profile.fuzz]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.0.0"
version = "2.1.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+8 -3
View File
@@ -4303,11 +4303,16 @@ int dc_msg_get_duration (const dc_msg_t* msg);
/**
* Check if a padlock should be shown beside the message.
* Check if message was correctly encrypted and signed.
*
* Historically, UIs showed a small padlock on the message then.
* Today, the UIs should instead
* show a small email-icon on the message if the message is not encrypted or signed,
* and nothing otherwise.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=padlock should be shown beside message, 0=do not show a padlock beside the message.
* @return 1=message correctly encrypted and signed, no need to show anything; 0=show email-icon beside the message.
*/
int dc_msg_get_showpadlock (const dc_msg_t* msg);
@@ -7419,7 +7424,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
/// "You left the group."
/// "You left."
///
/// Used in status messages.
#define DC_STR_GROUP_LEFT_BY_YOU 132
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.0.0"
version = "2.1.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"
+8
View File
@@ -224,6 +224,14 @@ impl CommandApi {
self.accounts.read().await.get_selected_account_id()
}
/// Set the order of accounts.
/// The provided list should contain all account IDs in the desired order.
/// If an account ID is missing from the list, it will be appended at the end.
/// If the list contains non-existent account IDs, they will be ignored.
async fn set_accounts_order(&self, order: Vec<u32>) -> Result<()> {
self.accounts.write().await.set_accounts_order(order).await
}
/// Get a list of all configured accounts.
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
let mut accounts = Vec::new();
+1 -1
View File
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.0.0"
"version": "2.1.0"
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.0.0"
version = "2.1.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.0.0"
version = "2.1.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.0.0"
version = "2.1.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.0.0"
"version": "2.1.0"
}
+3 -2
View File
@@ -25,16 +25,15 @@ skip = [
{ name = "derive_more-impl", version = "1.0.0" },
{ name = "derive_more", version = "1.0.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "generator", version = "0.7.5" },
{ name = "getrandom", version = "0.2.12" },
{ name = "hashbrown", version = "0.14.5" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "linux-raw-sys", version = "0.4.14" },
{ name = "loom", version = "0.5.6" },
{ name = "lru", version = "0.12.3" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "nom", version = "7.1.3" },
{ name = "proc-macro-crate", version = "2.0.0" },
{ name = "rand_chacha", version = "0.3.1" },
{ name = "rand_core", version = "0.6.4" },
{ name = "rand", version = "0.8.5" },
@@ -50,6 +49,7 @@ skip = [
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "toml_edit", version = "0.20.7" },
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },
@@ -67,6 +67,7 @@ skip = [
{ name = "windows_x86_64_gnu" },
{ name = "windows_x86_64_gnullvm" },
{ name = "windows_x86_64_msvc" },
{ name = "winnow", version = "0.5.40" },
{ name = "zerocopy", version = "0.7.32" },
]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.0.0"
version = "2.1.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"
+1 -1
View File
@@ -1 +1 @@
2025-07-09
2025-07-11
+58 -3
View File
@@ -1,6 +1,6 @@
//! # Account manager module.
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::future::Future;
use std::path::{Path, PathBuf};
@@ -270,9 +270,51 @@ impl Accounts {
}
}
/// Get a list of all account ids.
/// Gets a list of all account ids in the user-configured order.
pub fn get_all(&self) -> Vec<u32> {
self.accounts.keys().copied().collect()
let mut ordered_ids = Vec::new();
let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
// First, add accounts in the configured order
for &id in &self.config.inner.accounts_order {
if all_ids.remove(&id) {
ordered_ids.push(id);
}
}
// Then add any accounts not in the order list (newly added accounts)
for id in all_ids {
ordered_ids.push(id);
}
ordered_ids
}
/// Sets the order of accounts.
///
/// The provided list should contain all account IDs in the desired order.
/// If an account ID is missing from the list, it will be appended at the end.
/// If the list contains non-existent account IDs, they will be ignored.
pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
// Filter out non-existent account IDs
let mut filtered_order: Vec<u32> = order
.into_iter()
.filter(|id| existing_ids.contains(id))
.collect();
// Add any missing account IDs at the end
for &id in &existing_ids {
if !filtered_order.contains(&id) {
filtered_order.push(id);
}
}
self.config.inner.accounts_order = filtered_order;
self.config.sync().await?;
self.emit_event(EventType::AccountsChanged);
Ok(())
}
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
@@ -415,6 +457,10 @@ struct InnerConfig {
pub selected_account: u32,
pub next_id: u32,
pub accounts: Vec<AccountConfig>,
/// Ordered list of account IDs, representing the user's preferred order.
/// If an account ID is not in this list, it will be appended at the end.
#[serde(default)]
pub accounts_order: Vec<u32>,
}
impl Drop for Config {
@@ -481,6 +527,7 @@ impl Config {
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
accounts_order: Vec::new(),
};
if !lock {
let cfg = Self {
@@ -613,6 +660,10 @@ impl Config {
uuid,
});
self.inner.next_id += 1;
// Add new account to the end of the order list
self.inner.accounts_order.push(id);
id
};
@@ -634,6 +685,10 @@ impl Config {
// remove account from the configs
self.inner.accounts.remove(idx);
}
// Remove from order list as well
self.inner.accounts_order.retain(|&x| x != id);
if self.inner.selected_account == id {
// reset selected account
self.inner.selected_account = self
+69 -28
View File
@@ -42,9 +42,9 @@ use crate::smtp::send_msg_to_smtp;
use crate::stock_str;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
IsNoneOrEmpty, SystemTime, buf_compress, create_id, create_outgoing_rfc724_mid,
create_smeared_timestamp, create_smeared_timestamps, get_abs_path, gm2local_offset,
smeared_time, time, truncate_msg_text,
IsNoneOrEmpty, SystemTime, buf_compress, create_broadcast_shared_secret, create_id,
create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path,
gm2local_offset, smeared_time, time, truncate_msg_text,
};
use crate::webxdc::StatusUpdateSerial;
use crate::{chatlist_events, imap};
@@ -2905,10 +2905,12 @@ async fn prepare_send_msg(
// If the chat is a contact request, let the user accept it later.
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
}
// Allow to send "Member removed" messages so we can leave the group.
// Allow to send "Member removed" messages so we can leave the group/broadcast.
// Necessary checks should be made anyway before removing contact
// from the chat.
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
CantSendReason::NotAMember | CantSendReason::InBroadcast => {
msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup
}
CantSendReason::MissingKey => msg
.param
.get_bool(Param::ForcePlaintext)
@@ -3683,7 +3685,8 @@ pub async fn create_group_chat(
/// Returns the created chat's id.
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
let grpid = create_id();
create_broadcast_ex(context, Sync, grpid, chat_name).await
let secret = create_broadcast_shared_secret();
create_broadcast_ex(context, Sync, grpid, chat_name, secret).await
}
pub(crate) async fn create_broadcast_ex(
@@ -3691,6 +3694,7 @@ pub(crate) async fn create_broadcast_ex(
sync: sync::Sync,
grpid: String,
chat_name: String,
secret: String,
) -> Result<ChatId> {
let row_id = {
let chat_name = &chat_name;
@@ -3708,17 +3712,28 @@ pub(crate) async fn create_broadcast_ex(
},
)?);
}
let mut param = Params::new();
// param.set(Param::Unpromoted, 1); // TODO broadcasts will just never be unpromoted for now
param.set(Param::SymmetricKey, &secret);
t.execute(
"INSERT INTO chats \
(type, name, grpid, param, created_timestamp) \
VALUES(?, ?, ?, \'U=1\', ?);",
VALUES(?, ?, ?, ?, ?);",
(
Chattype::OutBroadcast,
&chat_name,
&grpid,
param.to_string(),
create_smeared_timestamp(context),
),
)?;
let chat_id = t.last_insert_rowid();
// TODO code duplication of `INSERT INTO broadcasts_shared_secrets`
t.execute(
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?)
ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.chat_id",
(chat_id, &secret),
)?;
Ok(t.last_insert_rowid().try_into()?)
};
context.sql.transaction(trans_fn).await?
@@ -3730,7 +3745,7 @@ pub(crate) async fn create_broadcast_ex(
if sync.into() {
let id = SyncId::Grpid(grpid);
let action = SyncAction::CreateBroadcast(chat_name);
let action = SyncAction::CreateBroadcast { chat_name, secret };
self::sync(context, id, action).await.log_err(context).ok();
}
@@ -3916,7 +3931,7 @@ pub(crate) async fn add_contact_to_chat_ex(
}
add_to_chat_contacts_table(context, time(), chat_id, &[contact_id]).await?;
}
if chat.typ == Chattype::Group && chat.is_promoted() {
if chat.is_promoted() {
msg.viewtype = Viewtype::Text;
let contact_addr = contact.get_addr().to_lowercase();
@@ -4079,8 +4094,6 @@ pub async fn remove_contact_from_chat(
"Cannot remove special contact"
);
let mut msg = Message::new(Viewtype::default());
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
if !chat.is_self_in_chat(context).await? {
@@ -4110,19 +4123,10 @@ pub async fn remove_contact_from_chat(
// in case of the database becoming inconsistent due to a bug.
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
if contact_id == ContactId::SELF {
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
} else {
msg.text =
stock_str::msg_del_member_local(context, contact_id, ContactId::SELF)
.await;
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
msg.param
.set(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
let res = send_msg(context, chat_id, &mut msg).await;
let addr = contact.get_addr();
let res = send_member_removal_msg(context, chat_id, contact_id, addr).await;
if contact_id == ContactId::SELF {
res?;
set_group_explicitly_left(context, &chat.grpid).await?;
@@ -4141,6 +4145,11 @@ pub async fn remove_contact_from_chat(
chat.sync_contacts(context).await.log_err(context).ok();
}
}
} else if chat.typ == Chattype::InBroadcast && contact_id == ContactId::SELF {
// For incoming broadcast channels, it's not possible to remove members,
// but it's possible to leave:
let self_addr = context.get_primary_self_addr().await?;
send_member_removal_msg(context, chat_id, contact_id, &self_addr).await?;
} else {
bail!("Cannot remove members from non-group chats.");
}
@@ -4148,6 +4157,28 @@ pub async fn remove_contact_from_chat(
Ok(())
}
async fn send_member_removal_msg(
context: &Context,
chat_id: ChatId,
contact_id: ContactId,
addr: &str,
) -> Result<MsgId> {
let mut msg = Message::new(Viewtype::Text);
if contact_id == ContactId::SELF {
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
} else {
msg.text = stock_str::msg_del_member_local(context, contact_id, ContactId::SELF).await;
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, addr.to_lowercase());
msg.param
.set(Param::ContactAddedRemoved, contact_id.to_u32());
send_msg(context, chat_id, &mut msg).await
}
async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()> {
if !is_group_explicitly_left(context, grpid).await? {
context
@@ -4930,7 +4961,10 @@ pub(crate) enum SyncAction {
SetVisibility(ChatVisibility),
SetMuted(MuteDuration),
/// Create broadcast channel with the given name.
CreateBroadcast(String),
CreateBroadcast {
chat_name: String,
secret: String,
},
Rename(String),
/// Set chat contacts by their addresses.
SetContacts(Vec<String>),
@@ -4993,8 +5027,15 @@ impl Context {
.id
}
SyncId::Grpid(grpid) => {
if let SyncAction::CreateBroadcast(name) = action {
create_broadcast_ex(self, Nosync, grpid.clone(), name.clone()).await?;
if let SyncAction::CreateBroadcast { chat_name, secret } = action {
create_broadcast_ex(
self,
Nosync,
grpid.clone(),
chat_name.clone(),
secret.to_string(),
)
.await?;
return Ok(());
}
get_chat_id_by_grpid(self, grpid)
@@ -5017,7 +5058,7 @@ impl Context {
SyncAction::Accept => chat_id.accept_ex(self, Nosync).await,
SyncAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
SyncAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
SyncAction::CreateBroadcast(_) => {
SyncAction::CreateBroadcast { .. } => {
Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
}
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
+163 -4
View File
@@ -5,7 +5,7 @@ use crate::ephemeral::Timer;
use crate::headerdef::HeaderDef;
use crate::imex::{ImexMode, has_backup, imex};
use crate::message::{MessengerMessage, delete_msgs};
use crate::mimeparser;
use crate::mimeparser::{self, MimeMessage};
use crate::receive_imf::receive_imf;
use crate::test_utils::{
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
@@ -374,7 +374,10 @@ 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_eq!(sent.load_from_db().await.get_text(), "You left the group.");
assert_eq!(
sent.load_from_db().await.get_text(),
stock_str::msg_group_left_local(&alice, ContactId::SELF).await
);
Ok(())
}
@@ -2931,6 +2934,147 @@ async fn test_broadcast_channel_protected_listid() -> Result<()> {
Ok(())
}
/// Test that if Bob leaves a broadcast channel,
/// Alice (the channel owner) won't see him as a member anymore,
/// but won't be notified about this in any way.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_leave_broadcast() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.section("Alice creates broadcast channel with Bob.");
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
let bob_contact = alice.add_or_lookup_contact(bob).await.id;
add_contact_to_chat(alice, alice_chat_id, bob_contact).await?;
tcm.section("Alice sends first message to broadcast.");
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
let bob_msg = bob.recv_msg(&sent_msg).await;
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1);
// Clear events so that we can later check
// that the 'Broadcast channel left' message didn't trigger IncomingMsg:
alice.evtracker.clear_events();
// Shift the time so that we can later check the "Broadcast channel left" message's timestamp:
SystemTime::shift(Duration::from_secs(60));
tcm.section("Bob leaves the broadcast channel.");
let bob_chat_id = bob_msg.chat_id;
bob_chat_id.accept(bob).await?;
remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?;
let leave_msg = bob.pop_sent_msg().await;
alice.recv_msg_trash(&leave_msg).await;
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 0);
alice.emit_event(EventType::Test);
alice
.evtracker
.get_matching(|ev| match ev {
EventType::Test => true,
EventType::IncomingMsg { .. } => {
panic!("'Broadcast channel left' message should be silent")
}
EventType::MsgsNoticed(..) => {
panic!("'Broadcast channel left' message shouldn't clear notifications")
}
EventType::MsgsChanged { .. } => {
panic!("Broadcast channels should be left silently, without any message");
}
_ => false,
})
.await;
Ok(())
}
/// Tests that if Bob leaves a broadcast channel with one device,
/// the other device shows a correct info message "You left.".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_leave_broadcast_multidevice() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob0 = &tcm.bob().await;
let bob1 = &tcm.bob().await;
tcm.section("Alice creates broadcast channel with Bob.");
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
let bob_contact = alice.add_or_lookup_contact(bob0).await.id;
add_contact_to_chat(alice, alice_chat_id, bob_contact).await?;
tcm.section("Alice sends first message to broadcast.");
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
let bob0_hello = bob0.recv_msg(&sent_msg).await;
let bob1_hello = bob1.recv_msg(&sent_msg).await;
tcm.section("Bob leaves the broadcast channel with his first device.");
let bob_chat_id = bob0_hello.chat_id;
bob_chat_id.accept(bob0).await?;
remove_contact_from_chat(bob0, bob_chat_id, ContactId::SELF).await?;
let leave_msg = bob0.pop_sent_msg().await;
let parsed = MimeMessage::from_bytes(bob1, leave_msg.payload().as_bytes(), None).await?;
assert_eq!(
parsed.parts[0].msg,
stock_str::msg_group_left_remote(bob0).await
);
let rcvd = bob1.recv_msg(&leave_msg).await;
assert_eq!(rcvd.chat_id, bob1_hello.chat_id);
assert!(rcvd.is_info());
assert_eq!(rcvd.get_info_type(), SystemMessage::MemberRemovedFromGroup);
assert_eq!(
rcvd.text,
stock_str::msg_group_left_local(bob1, ContactId::SELF).await
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypt_decrypt_broadcast_integration() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_without_secret = &tcm.bob().await;
let secret = "secret";
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
tcm.section("Create a broadcast channel with Bob, and send a message");
let alice_chat_id = create_broadcast(alice, "My Channel".to_string()).await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let mut alice_chat = Chat::load_from_db(alice, alice_chat_id).await?;
alice_chat.param.set(Param::SymmetricKey, secret);
alice_chat.update_param(alice).await?;
// TODO the chat_id 10 is magical here:
bob.sql
.execute(
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (10, ?)",
(secret,),
)
.await?;
let sent = alice
.send_text(alice_chat_id, "Symmetrically encrypted message")
.await;
let rcvd = bob.recv_msg(&sent).await;
assert_eq!(rcvd.text, "Symmetrically encrypted message");
tcm.section("If Bob doesn't know the secret, he can't decrypt the message");
bob_without_secret.recv_msg_trash(&sent).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_for_contact_with_blocked() -> Result<()> {
let t = TestContext::new().await;
@@ -3641,7 +3785,9 @@ async fn test_sync_broadcast() -> Result<()> {
assert_eq!(a1_broadcast_chat.get_name(), a0_broadcast_chat.get_name());
assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty());
add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
sync(alice0, alice1).await;
let sent = alice0.pop_sent_msg().await;
let rcvd = alice1.recv_msg(&sent).await;
dbg!(rcvd); // TODO
// This also imports Bob's key from the vCard.
// Otherwise it is possible that second device
@@ -3683,12 +3829,25 @@ async fn test_sync_name() -> Result<()> {
let a0_broadcast_id = create_broadcast(alice0, "Channel".to_string()).await?;
sync(alice0, alice1).await;
let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?;
set_chat_name(alice0, a0_broadcast_id, "Broadcast channel 42").await?;
sync(alice0, alice1).await;
//sync(alice0, alice1).await; // crash
let sent = alice0.pop_sent_msg().await;
let rcvd = alice1.recv_msg(&sent).await;
assert_eq!(rcvd.from_id, ContactId::SELF);
assert_eq!(rcvd.to_id, ContactId::SELF);
assert_eq!(
rcvd.text,
"You changed group name from \"Channel\" to \"Broadcast channel 42\"."
);
assert_eq!(rcvd.param.get_cmd(), SystemMessage::GroupNameChanged);
let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid)
.await?
.unwrap()
.0;
assert_eq!(rcvd.chat_id, a1_broadcast_id);
let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?;
assert_eq!(a1_broadcast_chat.get_type(), Chattype::OutBroadcast);
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast channel 42");
+4
View File
@@ -223,6 +223,10 @@ pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outloo
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
pub const WORSE_IMAGE_SIZE: u32 = 640;
/// Limit for received images size. Bigger images become `Viewtype::File` to avoid excessive memory
/// usage by UIs.
pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;
// Key for the folder configuration version (see below).
pub(crate) const DC_FOLDERS_CONFIGURED_KEY: &str = "folders_configured";
// this value can be increased if the folder configuration is changed and must be redone on next program start
+5 -4
View File
@@ -37,7 +37,7 @@ use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time};
use crate::{chat, chatlist_events, stock_str};
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
/// Time during which a contact is considered as seen recently.
const SEEN_RECENTLY_SECONDS: i64 = 600;
@@ -1922,9 +1922,10 @@ pub(crate) async fn mark_contact_id_as_verified(
contact_id: ContactId,
verifier_id: ContactId,
) -> Result<()> {
debug_assert_ne!(
contact_id, verifier_id,
"Contact cannot be verified by self"
ensure_and_debug_assert_ne!(
contact_id,
verifier_id,
"Contact cannot be verified by self",
);
context
.sql
+2 -1
View File
@@ -14,13 +14,14 @@ use crate::pgp;
pub fn try_decrypt<'a>(
mail: &'a ParsedMail<'a>,
private_keyring: &'a [SignedSecretKey],
symmetric_secrets: &[String],
) -> Result<Option<::pgp::composed::Message<'static>>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
};
let data = encrypted_data_part.get_body_raw()?;
let msg = pgp::pk_decrypt(data, private_keyring)?;
let msg = pgp::decrypt(data, private_keyring, symmetric_secrets)?;
Ok(Some(msg))
}
+19
View File
@@ -59,6 +59,25 @@ impl EncryptHelper {
Ok(ctext)
}
/// TODO documentation
pub async fn encrypt_for_broadcast(
self,
context: &Context,
passphrase: &str,
mail_to_encrypt: MimePart<'static>,
compress: bool,
) -> Result<String> {
let sign_key = load_self_secret_key(context).await?;
let mut raw_message = Vec::new();
let cursor = Cursor::new(&mut raw_message);
mail_to_encrypt.clone().write_part(cursor).ok();
let ctext = pgp::encrypt_for_broadcast(raw_message, passphrase, sign_key, compress).await?;
Ok(ctext)
}
/// Signs the passed-in `mail` using the private key from `context`.
/// Returns the payload and the signature.
pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result<String> {
+4
View File
@@ -93,6 +93,10 @@ pub enum HeaderDef {
/// This message obsoletes the text of the message defined here by rfc724_mid.
ChatEdit,
/// The secret shared amongst all recipients of this broadcast channel.
/// This secret.
ChatBroadcastSecret,
/// [Autocrypt](https://autocrypt.org/) header.
Autocrypt,
AutocryptGossip,
+65 -19
View File
@@ -20,6 +20,7 @@ use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::ensure_and_debug_assert;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::key::self_fingerprint;
use crate::key::{DcKey, SignedPublicKey};
@@ -308,7 +309,7 @@ impl MimeFactory {
} else if id == ContactId::SELF {
member_fingerprints.push(self_fingerprint.to_string());
} else {
debug_assert!(member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
ensure_and_debug_assert!(member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
}
}
member_timestamps.push(add_timestamp);
@@ -359,7 +360,7 @@ impl MimeFactory {
// if we are leaving the group.
past_member_fingerprints.push(self_fingerprint.to_string());
} else {
debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
ensure_and_debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
}
}
}
@@ -367,8 +368,8 @@ impl MimeFactory {
}
}
debug_assert!(member_timestamps.len() >= to.len());
debug_assert!(member_fingerprints.is_empty() || member_fingerprints.len() >= to.len());
ensure_and_debug_assert!(member_timestamps.len() >= to.len());
ensure_and_debug_assert!(member_fingerprints.is_empty() || member_fingerprints.len() >= to.len());
if to.len() > 1 {
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
@@ -445,7 +446,7 @@ impl MimeFactory {
};
let attach_selfavatar = Self::should_attach_selfavatar(context, &msg).await;
debug_assert!(
ensure_and_debug_assert!(
member_timestamps.is_empty()
|| to.len() + past_members.len() == member_timestamps.len()
);
@@ -668,7 +669,7 @@ impl MimeFactory {
));
}
debug_assert!(
ensure_and_debug_assert!(
self.member_timestamps.is_empty()
|| to.len() + past_members.len() == self.member_timestamps.len()
);
@@ -788,8 +789,8 @@ impl MimeFactory {
}
}
if let Loaded::Message { chat, .. } = &self.loaded {
if chat.typ == Chattype::OutBroadcast {
if let Loaded::Message { msg, chat } = &self.loaded {
if chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast {
headers.push((
"List-ID",
mail_builder::headers::text::Text::new(format!(
@@ -798,6 +799,15 @@ impl MimeFactory {
))
.into(),
));
if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup {
if let Some(secret) = chat.param.get(Param::SymmetricKey) {
headers.push((
"Chat-Broadcast-Secret",
mail_builder::headers::text::Text::new(secret.to_string()).into(),
));
}
}
}
}
@@ -978,6 +988,15 @@ impl MimeFactory {
} else {
unprotected_headers.push(header.clone());
}
} else if header_name == "chat-broadcast-secret" {
if is_encrypted {
protected_headers.push(header.clone());
} else {
warn!(
context,
"Message is unnecrypted, not including broadcast secret"
);
}
} else if is_encrypted {
protected_headers.push(header.clone());
@@ -1116,18 +1135,43 @@ impl MimeFactory {
Loaded::Mdn { .. } => true,
};
// Encrypt to self unconditionally,
// even for a single-device setup.
let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
encryption_keyring.extend(encryption_keys.iter().map(|(_addr, key)| (*key).clone()));
let symmetric_key = match &self.loaded {
Loaded::Message { chat, .. } if chat.typ == Chattype::OutBroadcast => {
// If there is no symmetric key yet
// (because this is an old broadcast channel,
// created before we had symmetric encryption),
// we just encrypt asymmetrically.
// Symmetric encryption exists since 2025-08;
// some time after that, we can think about requiring everyone
// to switch to symmetrically-encrypted broadcast lists.
chat.param.get(Param::SymmetricKey)
}
_ => None,
};
let encrypted = if let Some(symmetric_key) = symmetric_key {
info!(context, "Symmetrically encrypting for broadcast channel.");
encrypt_helper
.encrypt_for_broadcast(context, symmetric_key, message, compress)
.await?
} else {
// Asymmetric encryption
// Encrypt to self unconditionally,
// even for a single-device setup.
let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
encryption_keyring
.extend(encryption_keys.iter().map(|(_addr, key)| (*key).clone()));
encrypt_helper
.encrypt(context, encryption_keyring, message, compress)
.await?
};
// XXX: additional newline is needed
// to pass filtermail at
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>
let encrypted = encrypt_helper
.encrypt(context, encryption_keyring, message, compress)
.await?
+ "\n";
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>:
let encrypted = encrypted + "\n";
// Set the appropriate Content-Type for the outer message
MimePart::new(
@@ -1319,7 +1363,10 @@ impl MimeFactory {
}
}
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
if chat.typ == Chattype::Group
|| chat.typ == Chattype::OutBroadcast
|| chat.typ == Chattype::InBroadcast
{
headers.push((
"Chat-Group-Name",
mail_builder::headers::text::Text::new(chat.name.to_string()).into(),
@@ -1357,7 +1404,6 @@ impl MimeFactory {
}
}
SystemMessage::MemberAddedToGroup => {
ensure!(chat.typ != Chattype::OutBroadcast);
// TODO: lookup the contact by ID rather than email address.
// We are adding key-contacts, the cannot be looked up by address.
let email_to_add = msg.param.get(Param::Arg).unwrap_or_default();
+65 -45
View File
@@ -333,50 +333,63 @@ impl MimeMessage {
let mail_raw; // Memory location for a possible decrypted message.
let decrypted_msg; // Decrypted signed OpenPGP message.
let symmetric_secrets: Vec<String> = context
.sql
.query_map(
"SELECT secret FROM broadcasts_shared_secrets",
(),
|row| row.get(0),
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
let (mail, is_encrypted) =
match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
Ok(Some(mut msg)) => {
mail_raw = msg.as_data_vec().unwrap_or_default();
let (mail, is_encrypted) = match tokio::task::block_in_place(|| {
try_decrypt(&mail, &private_keyring, &symmetric_secrets)
}) {
Ok(Some(mut msg)) => {
mail_raw = msg.as_data_vec().unwrap_or_default();
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
}
decrypted_msg = Some(msg);
timestamp_sent = Self::get_timestamp_sent(
&decrypted_mail.headers,
timestamp_sent,
timestamp_rcvd,
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
}
if let Some(protected_aheader_value) = decrypted_mail
.headers
.get_header_value(HeaderDef::Autocrypt)
{
aheader_value = Some(protected_aheader_value);
}
decrypted_msg = Some(msg);
(Ok(decrypted_mail), true)
timestamp_sent = Self::get_timestamp_sent(
&decrypted_mail.headers,
timestamp_sent,
timestamp_rcvd,
);
if let Some(protected_aheader_value) = decrypted_mail
.headers
.get_header_value(HeaderDef::Autocrypt)
{
aheader_value = Some(protected_aheader_value);
}
Ok(None) => {
mail_raw = Vec::new();
decrypted_msg = None;
(Ok(mail), false)
}
Err(err) => {
mail_raw = Vec::new();
decrypted_msg = None;
warn!(context, "decryption failed: {:#}", err);
(Err(err), false)
}
};
(Ok(decrypted_mail), true)
}
Ok(None) => {
mail_raw = Vec::new();
decrypted_msg = None;
(Ok(mail), false)
}
Err(err) => {
mail_raw = Vec::new();
decrypted_msg = None;
warn!(context, "decryption failed: {:#}", err);
(Err(err), false)
}
};
let autocrypt_header = if !incoming {
None
@@ -1393,6 +1406,20 @@ impl MimeMessage {
} else {
Viewtype::File
}
} else if msg_type == Viewtype::Image
|| msg_type == Viewtype::Gif
|| msg_type == Viewtype::Sticker
{
match get_filemeta(decoded_data) {
// image size is known, not too big, keep msg_type:
Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
part.param.set_i64(Param::Width, width.into());
part.param.set_i64(Param::Height, height.into());
msg_type
}
// image is too big or size is unknown, display as file:
_ => Viewtype::File,
}
} else {
msg_type
};
@@ -1413,13 +1440,6 @@ impl MimeMessage {
};
info!(context, "added blobfile: {:?}", blob.as_name());
if mime_type.type_() == mime::IMAGE {
if let Ok((width, height)) = get_filemeta(decoded_data) {
part.param.set_int(Param::Width, width as i32);
part.param.set_int(Param::Height, height as i32);
}
}
part.typ = msg_type;
part.org_filename = Some(filename.to_string());
part.mimetype = Some(mime_type);
+48 -27
View File
@@ -835,15 +835,7 @@ Content-Transfer-Encoding: base64
Content-Disposition: inline;
filename="JPEG_filename.jpg"
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFuG0UUhr/szuxkxtlduzVx00jGUAhcI56CF0DiHXgFHqDiMfoCvAJC4gIJEBdVaZoGOY5dJ45r7zozmRlvzIVDm3Iu//PpP7/+s/OC/0+UREkEGZGr5N6mAX78MwISQEYJ6j6QYqZf7fzsYyRGJDISuQ9g6uMjW00WRCSRCP4DwNSEg496v828fC++B4yBGro7h+PliiiJHtR9B1vrmbtg359cOk+UqA8cDJm+eHu8yDfii6sAxK0u3hlsnPFn1WvT4XxYUqqtKu8cTMNKT8+nz3/aaWft3svKecBD/O9ETu2O//G7fXt5mHX2xwGP32aIEUNNvX/uh3z2pAkcKD+9XOLVNoMESw7YC+aPP8nqqSluyiuVaZRXCKLEQK3PxsP27UHe1lXV9eczCu2V8ippJA2gz+YTNTK9ttZOoYr+aeXwHp+k245cnFRNt1tmMJg3XV0dXQd3F5LcGn5fDnVQgwINRyFvtddnNmwBSW1qXfzNSDwoM+2A+aTbShfDd89Ka9ybiiLvGa33gK8THrWfVNUSSGLTALMRo/xBXnLbACu3QtRXY+sgkWnawKxCcLjPtr29gVZdPTidBUcSaXJeTfUL+mW4631Fmse+772xIGjSza/KTW/MYE/UCXPbEWs//Xxvqf8qZgYhG0jt8cnTnJpEgfDQ6rvE0n9tq66ICafil5OnOYt0hY94FUPoxI136uPpw4VIYNd+M5utrqvGpg0hc50bRl6BvLanX4pb/3347uWi/WgNHtYyyrdwszsX5fjZtwghy7C5sK2WgUoErcjIoEWr4fAHHhoRP+XZ86L/uMAarLFgsMBaAJhM8Ief9Ey7yHSmyXTp0GRoF7KQEdD/ArmtONKkgCleAAAAAElFTkSuQmCC
----11019878869865180--
@@ -908,15 +900,7 @@ Content-ID: <part1.9DFA679B.52A88D69@example.org>
Content-Disposition: inline;
filename="1.png"
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFuG0UUhr/szuxkxtlduzVx00jGUAhcI56CF0DiHXgFHqDiMfoCvAJC4gIJEBdVaZoGOY5dJ45r7zozmRlvzIVDm3Iu//PpP7/+s/OC/0+UREkEGZGr5N6mAX78MwISQEYJ6j6QYqZf7fzsYyRGJDISuQ9g6uMjW00WRCSRCP4DwNSEg496v828fC++B4yBGro7h+PliiiJHtR9B1vrmbtg359cOk+UqA8cDJm+eHu8yDfii6sAxK0u3hlsnPFn1WvT4XxYUqqtKu8cTMNKT8+nz3/aaWft3svKecBD/O9ETu2O//G7fXt5mHX2xwGP32aIEUNNvX/uh3z2pAkcKD+9XOLVNoMESw7YC+aPP8nqqSluyiuVaZRXCKLEQK3PxsP27UHe1lXV9eczCu2V8ippJA2gz+YTNTK9ttZOoYr+aeXwHp+k245cnFRNt1tmMJg3XV0dXQd3F5LcGn5fDnVQgwINRyFvtddnNmwBSW1qXfzNSDwoM+2A+aTbShfDd89Ka9ybiiLvGa33gK8THrWfVNUSSGLTALMRo/xBXnLbACu3QtRXY+sgkWnawKxCcLjPtr29gVZdPTidBUcSaXJeTfUL+mW4631Fmse+772xIGjSza/KTW/MYE/UCXPbEWs//Xxvqf8qZgYhG0jt8cnTnJpEgfDQ6rvE0n9tq66ICafil5OnOYt0hY94FUPoxI136uPpw4VIYNd+M5utrqvGpg0hc50bRl6BvLanX4pb/3347uWi/WgNHtYyyrdwszsX5fjZtwghy7C5sK2WgUoErcjIoEWr4fAHHhoRP+XZ86L/uMAarLFgsMBaAJhM8Ief9Ey7yHSmyXTp0GRoF7KQEdD/ArmtONKkgCleAAAAAElFTkSuQmCC
--------------10CC6C2609EB38DA782C5CA9--
--------------779C1631600DF3DB8C02E53A--"#;
@@ -979,15 +963,7 @@ Content-Type: image/jpeg;
Content-Transfer-Encoding: base64
Content-ID: <image001.jpg@01D622B3.C9D8D750>
ISVb1L3m7z15Wy5w97a2cJg6W8P8YKOYfWn3PJ/UCSFcvCPtvBhcXieiN3M3ljguzG4XK7BnGgxG
acAQdY8e0cWz1n+zKPNeNn4Iu3GXAXz4/IPksHk54inl1//0Lv8ggZjljfjnf0q1SPftYI7lpZWT
/4aTCkimRrAIcwrQJPnZJRb7BPSC6kfn1QJHMv77mRMz2+4WbdfpyPQQ0CWLJsgVXtBsSMf2Awal
n+zZzhGpXyCbWTEw1ccqZcK5KaiKNqWv51N4yVXw9dzJoCvxbYtCFGZZJdx7c+ObDotaF1/9KY4C
xJjgK9/NgTXCZP1jYm0XIBnJsFSNg0pnMRETttTuGbOVi1/s/F1RGv5RNZsCUt21d9FhkWQQXsd2
rOzDgTdag6BQCN3hSU9eKW/GhNBuMibRN9eS7Sm1y2qFU1HgGJBQfPPRPLKxXaNi++Zt0tnon2IU
8pg5rP/IvStXYQNUQ9SiFdfAUkLU5b1j8ltnka8xl+oXsleSG44GPz6kM0RmwUrGkl4z/+NfHSsI
K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
iVBORw0KGgoAAAANSUhEUgAAACAAAAAeCAAAAABoYUP1AAAAAXNSR0IArs4c6QAAAo1JREFUKJFdkdFuG0UUhr/szuxkxtlduzVx00jGUAhcI56CF0DiHXgFHqDiMfoCvAJC4gIJEBdVaZoGOY5dJ45r7zozmRlvzIVDm3Iu//PpP7/+s/OC/0+UREkEGZGr5N6mAX78MwISQEYJ6j6QYqZf7fzsYyRGJDISuQ9g6uMjW00WRCSRCP4DwNSEg496v828fC++B4yBGro7h+PliiiJHtR9B1vrmbtg359cOk+UqA8cDJm+eHu8yDfii6sAxK0u3hlsnPFn1WvT4XxYUqqtKu8cTMNKT8+nz3/aaWft3svKecBD/O9ETu2O//G7fXt5mHX2xwGP32aIEUNNvX/uh3z2pAkcKD+9XOLVNoMESw7YC+aPP8nqqSluyiuVaZRXCKLEQK3PxsP27UHe1lXV9eczCu2V8ippJA2gz+YTNTK9ttZOoYr+aeXwHp+k245cnFRNt1tmMJg3XV0dXQd3F5LcGn5fDnVQgwINRyFvtddnNmwBSW1qXfzNSDwoM+2A+aTbShfDd89Ka9ybiiLvGa33gK8THrWfVNUSSGLTALMRo/xBXnLbACu3QtRXY+sgkWnawKxCcLjPtr29gVZdPTidBUcSaXJeTfUL+mW4631Fmse+772xIGjSza/KTW/MYE/UCXPbEWs//Xxvqf8qZgYhG0jt8cnTnJpEgfDQ6rvE0n9tq66ICafil5OnOYt0hY94FUPoxI136uPpw4VIYNd+M5utrqvGpg0hc50bRl6BvLanX4pb/3347uWi/WgNHtYyyrdwszsX5fjZtwghy7C5sK2WgUoErcjIoEWr4fAHHhoRP+XZ86L/uMAarLFgsMBaAJhM8Ief9Ey7yHSmyXTp0GRoF7KQEdD/ArmtONKkgCleAAAAAElFTkSuQmCC
------=_NextPart_000_0003_01D622B3.CA753E60--
"#;
@@ -2022,3 +1998,48 @@ async fn test_protected_date() -> Result<()> {
assert_eq!(alice_msg.get_timestamp(), bob_msg.get_timestamp());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_huge_image_becomes_file() -> Result<()> {
let t = TestContext::new_alice().await;
let msg_id = receive_imf(
&t,
include_bytes!("../../test-data/message/image_huge_64M.eml"),
false,
)
.await?
.unwrap()
.msg_ids[0];
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
// Huge image should be treated as file:
assert_eq!(msg.viewtype, Viewtype::File);
assert!(msg.get_file(&t).is_some());
assert_eq!(msg.get_filename().unwrap(), "huge_image.png");
assert_eq!(msg.get_filemime().unwrap(), "image/png");
// File has no width or height
assert!(msg.param.get_int(Param::Width).is_none());
assert!(msg.param.get_int(Param::Height).is_none());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_4k_image_stays_image() -> Result<()> {
let t = TestContext::new_alice().await;
let msg_id = receive_imf(
&t,
include_bytes!("../../test-data/message/image_4k.eml"),
false,
)
.await?
.unwrap()
.msg_ids[0];
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
// 4K image should be treated as image:
assert_eq!(msg.viewtype, Viewtype::Image);
assert!(msg.get_file(&t).is_some());
assert_eq!(msg.get_filename().unwrap(), "4k_image.png");
assert_eq!(msg.get_filemime().unwrap(), "image/png");
assert_eq!(msg.param.get_int(Param::Width).unwrap_or_default(), 3840);
assert_eq!(msg.param.get_int(Param::Height).unwrap_or_default(), 2160);
Ok(())
}
+7
View File
@@ -169,6 +169,13 @@ pub enum Param {
/// post something to the mailing list.
ListPost = b'p',
/// For Chats and Messages:
/// For chats of type [`Chattype::OutBroadcast`] and [`Chattype::InBroadcast`] // TODO (or just OutBroadcast)
/// and for messages adding members to such a chat.
/// The symmetric key shared among all chat participants,
/// used to encrypt and decrypt messages.
SymmetricKey = b'z', // TODO remove this again
/// For Contacts: If this is the List-Post address of a mailing list, contains
/// the List-Id of the mailing list (which is also used as the group id of the chat).
ListId = b's',
+78 -5
View File
@@ -12,6 +12,7 @@ use pgp::composed::{
SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
StandaloneSignature, SubkeyParamsBuilder, TheRing,
};
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
use pgp::crypto::ecc_curve::ECCCurve;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
@@ -235,9 +236,10 @@ pub fn pk_calc_signature(
///
/// Receiver private keys are provided in
/// `private_keys_for_decryption`.
pub fn pk_decrypt(
pub fn decrypt(
ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey],
symmetric_secrets: &[String],
) -> Result<pgp::composed::Message<'static>> {
let cursor = Cursor::new(ctext);
let (msg, _headers) = Message::from_armor(cursor)?;
@@ -245,10 +247,17 @@ pub fn pk_decrypt(
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
let empty_pw = Password::empty();
// TODO it may degrade performance that we always try out all passwords here
let message_password: Vec<Password> = symmetric_secrets
.iter()
.map(|p| Password::from(p.as_str()))
.collect();
let message_password: Vec<&Password> = message_password.iter().collect();
let ring = TheRing {
secret_keys: skeys,
key_passwords: vec![&empty_pw],
message_password: vec![],
message_password,
session_keys: vec![],
allow_legacy: false,
};
@@ -311,7 +320,7 @@ pub async fn symm_encrypt(passphrase: &str, plain: Vec<u8>) -> Result<String> {
tokio::task::spawn_blocking(move || {
let mut rng = thread_rng();
let s2k = StringToKey::new_default(&mut rng);
let builder = MessageBuilder::from_bytes("", plain);
let builder: MessageBuilder<'_> = MessageBuilder::from_bytes("", plain);
let mut builder = builder.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
builder.encrypt_with_password(s2k, &passphrase)?;
@@ -322,6 +331,39 @@ pub async fn symm_encrypt(passphrase: &str, plain: Vec<u8>) -> Result<String> {
.await?
}
/// Symmetric encryption.
pub async fn encrypt_for_broadcast(
plain: Vec<u8>,
passphrase: &str,
private_key_for_signing: SignedSecretKey,
compress: bool,
) -> Result<String> {
let passphrase = Password::from(passphrase.to_string());
tokio::task::spawn_blocking(move || {
let mut rng = thread_rng();
let s2k = StringToKey::new_default(&mut rng);
let msg = MessageBuilder::from_bytes("", plain);
let mut msg = msg.seipd_v2(
&mut rng,
SymmetricKeyAlgorithm::AES128,
AeadAlgorithm::Ocb,
ChunkSize::C8KiB,
);
msg.encrypt_with_password(&mut rng, s2k, &passphrase)?;
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
Ok(encoded_msg)
})
.await?
}
/// Symmetric decryption.
pub async fn symm_decrypt<T: BufRead + std::fmt::Debug + 'static + Send>(
passphrase: &str,
@@ -345,7 +387,10 @@ mod tests {
use tokio::sync::OnceCell;
use super::*;
use crate::test_utils::{alice_keypair, bob_keypair};
use crate::{
key::load_self_secret_key,
test_utils::{TestContextManager, alice_keypair, bob_keypair},
};
fn pk_decrypt_and_validate<'a>(
ctext: &'a [u8],
@@ -356,7 +401,7 @@ mod tests {
HashSet<Fingerprint>,
Vec<u8>,
)> {
let mut msg = pk_decrypt(ctext.to_vec(), private_keys_for_decryption)?;
let mut msg = decrypt(ctext.to_vec(), private_keys_for_decryption, &[])?;
let content = msg.as_data_vec()?;
let ret_signature_fingerprints =
valid_signature_fingerprints(&msg, public_keys_for_validation)?;
@@ -542,4 +587,32 @@ mod tests {
assert_eq!(content, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypt_decrypt_broadcast() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let plain = Vec::from(b"this is the secret message");
let shared_secret = "shared secret";
let ctext = encrypt_for_broadcast(
plain.clone(),
shared_secret,
load_self_secret_key(alice).await?,
true,
)
.await?;
let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?;
let mut decrypted = decrypt(
ctext.into(),
&bob_private_keyring,
&[shared_secret.to_string()],
)?;
assert_eq!(decrypted.as_data_vec()?, plain);
Ok(())
}
}
+101 -12
View File
@@ -14,7 +14,9 @@ use mailparse::SingleInfo;
use num_traits::FromPrimitive;
use regex::Regex;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::chat::{
self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, remove_from_chat_contacts_table,
};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
use crate::contact::{Contact, ContactId, Origin, mark_contact_id_as_verified};
@@ -42,7 +44,7 @@ use crate::simplify;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{self, buf_compress, remove_subject_prefix};
use crate::{chatlist_events, location};
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
use crate::{contact, imap};
/// This is the struct that is returned after receiving one email (aka MIME message).
@@ -1454,7 +1456,7 @@ async fn do_chat_assignment(
false => None,
};
if let Some(chat) = chat {
debug_assert!(chat.typ == Chattype::Single);
ensure_and_debug_assert!(chat.typ == Chattype::Single);
let mut new_protection = match verified_encryption {
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
@@ -1538,10 +1540,25 @@ async fn do_chat_assignment(
if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await?
{
id
} else {
} else if let Some(secret) =
mime_parser.get_header(HeaderDef::ChatBroadcastSecret)
{
let name =
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
chat::create_broadcast_ex(context, Nosync, listid, name).await?
chat::create_broadcast_ex(
context,
Nosync,
listid,
name,
secret.to_string(),
)
.await?
} else {
warn!(
context,
"Unknown shared secret for outgoing broadcast (TRASH)"
);
DC_CHAT_ID_TRASH
},
);
}
@@ -1687,7 +1704,9 @@ async fn add_parts(
_ if chat.id.is_special() => GroupChangesInfo::default(),
Chattype::Single => GroupChangesInfo::default(),
Chattype::Mailinglist => GroupChangesInfo::default(),
Chattype::OutBroadcast => GroupChangesInfo::default(),
Chattype::OutBroadcast => {
apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
}
Chattype::Group => {
apply_group_changes(
context,
@@ -1701,7 +1720,7 @@ async fn add_parts(
.await?
}
Chattype::InBroadcast => {
apply_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
}
};
@@ -2137,7 +2156,7 @@ RETURNING id
// afterwards insert additional parts.
replace_msg_id = None;
debug_assert!(!row_id.is_special());
ensure_and_debug_assert!(!row_id.is_special());
created_db_entries.push(row_id);
}
@@ -2400,7 +2419,9 @@ async fn lookup_chat_by_reply(
// lookup by reply should never be needed
// as we can directly assign the message to the chat
// by its group ID.
debug_assert!(mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted());
ensure_and_debug_assert!(
mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted()
);
// Try to assign message to the same chat as the parent message.
let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
@@ -3438,7 +3459,55 @@ async fn apply_mailinglist_changes(
Ok(())
}
async fn apply_broadcast_changes(
async fn apply_out_broadcast_changes(
context: &Context,
mime_parser: &MimeMessage,
chat: &mut Chat,
from_id: ContactId,
) -> Result<GroupChangesInfo> {
// TODO code duplication with apply_in_broadcast_changes()
ensure!(chat.typ == Chattype::OutBroadcast);
let mut send_event_chat_modified = false;
let mut better_msg = None;
apply_chat_name_and_avatar_changes(
context,
mime_parser,
from_id,
chat,
&mut send_event_chat_modified,
&mut better_msg,
)
.await?;
if let Some(_removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
if from_id != ContactId::SELF {
// The sender of the message left the broadcast channel
remove_from_chat_contacts_table(context, chat.id, from_id).await?;
return Ok(GroupChangesInfo {
better_msg: Some("".to_string()),
added_removed_id: None,
silent: true,
extra_msgs: vec![],
});
}
}
if send_event_chat_modified {
context.emit_event(EventType::ChatModified(chat.id));
chatlist_events::emit_chatlist_item_changed(context, chat.id);
}
Ok(GroupChangesInfo {
better_msg,
added_removed_id: None,
silent: false,
extra_msgs: vec![],
})
}
async fn apply_in_broadcast_changes(
context: &Context,
mime_parser: &MimeMessage,
chat: &mut Chat,
@@ -3459,6 +3528,26 @@ async fn apply_broadcast_changes(
)
.await?;
if let Some(_removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
// The only member added/removed message that is ever sent is "I left.",
// so, this is the only case we need to handle here
if from_id == ContactId::SELF {
better_msg
.get_or_insert(stock_str::msg_group_left_local(context, ContactId::SELF).await);
}
}
if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
context
.sql
.execute(
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?)
ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.chat_id",
(chat.id, secret),
)
.await?;
}
if send_event_chat_modified {
context.emit_event(EventType::ChatModified(chat.id));
chatlist_events::emit_chatlist_item_changed(context, chat.id);
@@ -3727,7 +3816,7 @@ async fn add_or_lookup_key_contacts_by_address_list(
}
}
debug_assert_eq!(contact_ids.len(), address_list.len());
ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
Ok(contact_ids)
}
@@ -3881,7 +3970,7 @@ async fn lookup_key_contacts_by_address_list(
contact_ids.push(contact_id);
}
}
debug_assert_eq!(address_list.len(), contact_ids.len());
ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
Ok(contact_ids)
}
+12
View File
@@ -1251,6 +1251,18 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
.await?;
}
inc_and_check(&mut migration_version, 133)?;
if dbversion < migration_version {
sql.execute_migration(
"CREATE TABLE broadcasts_shared_secrets(
chat_id INTEGER PRIMARY KEY NOT NULL, -- TODO we don't actually need the chat_id
secret TEXT NOT NULL
) STRICT",
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
+2 -2
View File
@@ -285,7 +285,7 @@ pub enum StockMessage {
#[strum(props(fallback = "Member %1$s removed by %2$s."))]
MsgDelMemberBy = 131,
#[strum(props(fallback = "You left the group."))]
#[strum(props(fallback = "You left."))]
MsgYouLeftGroup = 132,
#[strum(props(fallback = "Group left by %1$s."))]
@@ -685,7 +685,7 @@ pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
translated(context, StockMessage::MsgILeftGroup).await
}
/// Stock string: `You left the group.` or `Group left by %1$s.`.
/// Stock string: `You left.` or `Group left by %1$s.`.
pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouLeftGroup).await
+22
View File
@@ -2,6 +2,7 @@
//!
//! This private module is only compiled for test runs.
use std::collections::{BTreeMap, HashSet};
use std::env::current_dir;
use std::fmt::Write;
use std::ops::{Deref, DerefMut};
use std::panic;
@@ -1063,6 +1064,27 @@ impl Drop for TestContext {
// Print the chats if runtime still exists.
handle.block_on(async move {
self.print_chats().await;
// If you set this to true, and a test fails,
// the sql databases will be saved into the current working directory
// so that you can examine them.
if std::env::var("DELTACHAT_SAVE_TMP_DB").is_ok() {
let _: u32 = self
.sql
.query_get_value("PRAGMA wal_checkpoint;", ())
.await
.unwrap()
.unwrap();
let from = self.get_dbfile();
let target = current_dir()
.unwrap()
.join(format!("test-account-{}.db", self.name()));
tokio::fs::copy(from, &target).await.unwrap();
eprintln!("Copied database from {from:?} to {target:?}\n");
} else {
eprintln!("Hint: If you want to examine the database files, set environment variable DELTACHAT_SAVE_TMP_DB=1\n")
}
});
}
});
+58
View File
@@ -300,6 +300,26 @@ pub(crate) fn create_id() -> String {
base64::engine::general_purpose::URL_SAFE.encode(arr)
}
/// Generate a shared secret for a broadcast channel, consisting of 64 characters..
///
/// The string generated by this function has 384 bits of entropy
/// and is returned as 64 Base64 characters, each containing 6 bits of entropy.
/// 384 is chosen because it is sufficiently secure
/// (larger than AES-128 keys used for message encryption)
/// TODO: Is it still true that we use AES-128? This info is taken from create_id() comment above.
/// and divides both by 8 (byte size) and 6 (number of bits in a single Base64 character).
// TODO ask someone what a good size would be here
pub(crate) fn create_broadcast_shared_secret() -> String {
// ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure.
let mut rng = thread_rng();
// Generate 384 random bits.
let mut arr = [0u8; 48];
rng.fill(&mut arr[..]);
base64::engine::general_purpose::URL_SAFE.encode(arr)
}
/// Returns true if given string is a valid ID.
///
/// All IDs generated with `create_id()` should be considered valid.
@@ -763,5 +783,43 @@ pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
Ok(())
}
/// Returns early with an error if a condition is not satisfied.
/// In non-optimized builds, panics instead if so.
#[macro_export]
macro_rules! ensure_and_debug_assert {
($($arg:tt)*) => {
debug_assert!($($arg)*);
anyhow::ensure!($($arg)*);
};
}
/// Returns early with an error on two expressions inequality.
/// In non-optimized builds, panics instead if so.
#[macro_export]
macro_rules! ensure_and_debug_assert_eq {
($left:expr, $right:expr, $($arg:tt)*) => {
match (&$left, &$right) {
(left_val, right_val) => {
debug_assert_eq!(left_val, right_val, $($arg)*);
anyhow::ensure!(left_val == right_val, $($arg)*);
}
}
};
}
/// Returns early with an error on two expressions equality.
/// In non-optimized builds, panics instead if so.
#[macro_export]
macro_rules! ensure_and_debug_assert_ne {
($left:expr, $right:expr, $($arg:tt)*) => {
match (&$left, &$right) {
(left_val, right_val) => {
debug_assert_ne!(left_val, right_val, $($arg)*);
anyhow::ensure!(left_val != right_val, $($arg)*);
}
}
};
}
#[cfg(test)]
mod tools_tests;
@@ -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#11🔒: Me (Contact#Contact#Self): You left. [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]
--------------------------------------------------------------------------------
+28
View File
@@ -0,0 +1,28 @@
Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)
From: Test Sender <sender@testrun.org>
Subject: Large image test
To: alice@example.org
Message-ID: <big-image-test@testrun.org>
Date: Thu, 17 Dec 2020 15:38:45 +0100
User-Agent: Test-Agent/1.0
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="------------BIG_IMAGE_BOUNDARY"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------BIG_IMAGE_BOUNDARY
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
Here is a 4k image
--------------BIG_IMAGE_BOUNDARY
Content-Type: image/png;
name="4k_image.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="4k_image.png"
iVBORw0KGgoAAAANSUhEUgAADwAAAAhwCAIAAAAf3FwlAAAAEElEQVR4nAEFAPr/AP////8AAAAFCeiupAAAAABJRU5ErkJggg==
--------------BIG_IMAGE_BOUNDARY--
+28
View File
@@ -0,0 +1,28 @@
Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)
From: Test Sender <sender@testrun.org>
Subject: Large image test
To: alice@example.org
Message-ID: <huge-image-test@testrun.org>
Date: Thu, 17 Dec 2020 15:38:45 +0100
User-Agent: Test-Agent/1.0
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="------------HUGE_IMAGE_BOUNDARY"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------HUGE_IMAGE_BOUNDARY
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
Here is a huge image
--------------HUGE_IMAGE_BOUNDARY
Content-Type: image/png;
name="huge_image.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="huge_image.png"
iVBORw0KGgoAAAANSUhEUgAAH0AAAB9ACAIAAACJkzqjAAAAEElEQVR4nAEFAPr/AP////8AAAAFCeiupAAAAABJRU5ErkJggg==
--------------HUGE_IMAGE_BOUNDARY--