Compare commits

..

15 Commits

Author SHA1 Message Date
Septias
274c8f6fdf add test 2025-07-17 12:22:20 +02:00
cliffmccarthy
e03e2d9a68 fix: List e-mail contacts in repl listcontacts command
- After the revisions to support key contacts, the 'listcontacts'
  command in the repl only lists key-contacts.  A separate flag now
  has to be passed to Contact::get_all() to list address contacts.
  This makes it difficult to run the example in README.md because we
  need to see the new contact so we can get its ID for the next
  command in the example, that creates a chat by ID.
- Revised 'listcontacts' command to make a second call to
  Contact::get_all() with the DC_GCL_ADDRESS flag, then print the
  e-mail contacts after the key contacts.
- Revised configuration example in top-level README.md to reflect
  current command output.

fixes #7011
2025-07-17 10:27:14 +02:00
iequidoo
8fc6ea19b4 feat: {ensure_and,logged}_debug_assert: Don't evaluate condition twice 2025-07-16 11:41:45 -03:00
iequidoo
c5c947e175 feat(repl): Print errors and debug logs to stderr
Follow-up to 545007aca5.
2025-07-16 11:39:56 -03:00
iequidoo
6d8dff54a7 fix: Ignore protected headers in outer message part (#6357)
Delta Chat always adds protected headers to the inner encrypted or signed message, so if a protected
header is only present in the outer part, it should be ignored because it's probably added by the
server or somebody else. The exceptions are Subject and List-ID because there are known cases when
they are only present in the outer message part.

Also treat any Chat-* headers as protected. This fixes e.g. a case when the server injects a
"Chat-Version" IMF header tricking Delta Chat into thinking that it's a chat message.

Also handle "Auto-Submitted" and "Autocrypt-Setup-Message" as protected headers on the receiver
side, this was apparently forgotten.
2025-07-16 11:39:06 -03:00
link2xt
a0f6bdffeb chore(release): prepare for 2.2.0 2025-07-14 18:43:03 +00:00
link2xt
e6fd52afff fix: always prefer the last header
Headers are normally added at the top of the message,
e.g. when forwarding new `Received` headers are
added at the top.

When headers are protected with DKIM-Signature
and oversigning is not used,
forged headers may be added on top
so headers from the top are generally less trustworthy.

This is tested with `test_take_last_header`,
but so far last header was only preferred
for known headers. This change extends
preference of the last header to all headers.
2025-07-14 14:57:52 +00:00
Nico de Haen
0142515887 api!: In ChatListItem, replace is_group and is_(out_)broadcast with chat_type property (#7003)
- removed ChatListItem.is_broadcast
- mark ChatListItem.is_group as deprecated
2025-07-14 11:16:28 +02:00
link2xt
d45ec7f34d feat: advance next UID even if connection fails while fetching
Connection sometimes fails while processing FETCH
responses. In this case `fetch_new_messages` exits early
and does not advance next expected UID even if
some messages were processed.

This results in prefetching the same messages
after reconnection and log messages
similar to
"Not moving the message ab05c85a-e191-4fd2-a951-9972bc7e167f@localhost that we have seen before.".

With this change we advance next expected UID
even if `fetch_new_messages` returns a network error.
2025-07-13 16:32:26 +00:00
iequidoo
752f45f0f0 test: Unencrypted group creation (#6927) 2025-07-13 12:59:33 -03:00
iequidoo
0299543a86 api(jsonrpc): Add CommandApi::create_group_chat_unencrypted() (#6927) 2025-07-13 12:59:33 -03:00
iequidoo
d3908d6b36 api: Add chat::create_group_ex(), deprecate create_group_chat() (#6927)
`chat::create_group_ex()` gains an `encryption: Option<ProtectionStatus>` parameter to support
unencrypted chats.
2025-07-13 12:59:33 -03:00
iequidoo
2cf979de53 feat: Donation request device message (#6913)
A donation request device message is added if >= 100 messages have been sent and delivered. The
condition is checked every 30 days since the first message is sent. The message is added only once.
2025-07-13 11:53:14 -03:00
Hocuri
f5e8c8083d test: Tune down DELTACHAT_SAVE_TMP_DB hint (#6998)
Follow-up for https://github.com/chatmail/core/pull/6992

Since we're printing the hint to stderr now, rather than stdout (as per
link2xt's suggestion), it was too noisy. Also, it was printed once for
every test account rather than once per test.

Now, it integrates nicely with rust's hint to enable a backtrace:

```
  stderr ───

    thread 'chat::chat_tests::test_broadcasts_name_and_avatar' panicked at src/chat/chat_tests.rs:2757:5:
    assertion failed: `(left == right)`

    Diff < left / right > :
    <true
    >false


    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    note: If you want to examine the database files, set environment variable DELTACHAT_SAVE_TMP_DB=1

        FAIL [   0.265s] deltachat chat::chat_tests::test_broadcast
```
2025-07-12 12:52:04 +02:00
iequidoo
58b99f59f7 feat: Log failed debug assertions in all configurations
Add `logged_debug_assert` macro logging a warning if a condition is not satisfied, before invoking
`debug_assert!`, and use this macro where `Context` is accessible (i.e. don't change function
signatures for now).
Follow-up to 0359481ba4.
2025-07-12 07:27:55 -03:00
35 changed files with 468 additions and 182 deletions

View File

@@ -1,5 +1,28 @@
# Changelog
## [2.2.0] - 2025-07-14
### API-Changes
- Add chat::create_group_ex(), deprecate create_group_chat() ([#6927](https://github.com/chatmail/core/pull/6927)).
- jsonrpc: Add CommandApi::create_group_chat_unencrypted() ([#6927](https://github.com/chatmail/core/pull/6927)).
- [**breaking**] In ChatListItem, replace is_group and is_(out_)broadcast with chat_type property ([#7003](https://github.com/chatmail/core/pull/7003)).
### Features / Changes
- Log failed debug assertions in all configurations.
- Donation request device message ([#6913](https://github.com/chatmail/core/pull/6913)).
- Advance next UID even if connection fails while fetching.
### Fixes
- Always prefer the last header.
### Tests
- Tune down DELTACHAT_SAVE_TMP_DB hint ([#6998](https://github.com/chatmail/core/pull/6998)).
- Unencrypted group creation ([#6927](https://github.com/chatmail/core/pull/6927)).
## [2.1.0] - 2025-07-11
### Features / Changes
@@ -6454,3 +6477,4 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[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
[2.2.0]: https://github.com/chatmail/core/compare/v2.1.0..v2.2.0

10
Cargo.lock generated
View File

@@ -1285,7 +1285,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.1.0"
version = "2.2.0"
dependencies = [
"anyhow",
"async-broadcast",
@@ -1395,7 +1395,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.1.0"
version = "2.2.0"
dependencies = [
"anyhow",
"async-channel 2.3.1",
@@ -1417,7 +1417,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.1.0"
version = "2.2.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1433,7 +1433,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.1.0"
version = "2.2.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1462,7 +1462,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.1.0"
version = "2.2.0"
dependencies = [
"anyhow",
"deltachat",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.1.0"
version = "2.2.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.85"

View File

@@ -84,26 +84,29 @@ Create a contact:
```
> addcontact yourfriends@email.org
Command executed successfully.
```
List contacts:
```
> listcontacts
Contact#10: <name unset> <yourfriends@email.org>
Contact#1: Me √√ <your@email.org>
Contact#Contact#Self: Me √ <your@email.org>
1 key contacts.
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
1 address contacts.
```
Create a chat with your friend and send a message:
```
> createchat 10
Single#10 created successfully.
> chat 10
Single#10: yourfriends@email.org [yourfriends@email.org]
Single#Chat#12 created successfully.
> chat 12
Selecting chat Chat#12
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
0 messages.
81.252µs to create this list, 123.625µs to mark all messages as noticed.
> send hi
Message sent.
```
List messages when inside a chat:

View File

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

View File

@@ -7667,6 +7667,9 @@ void dc_event_unref(dc_event_t* event);
/// @deprecated 2025-06-05
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
#define DC_STR_DONATION_REQUEST 193
/// "Contact". Deprecated, currently unused.
#define DC_STR_CONTACT 200

View File

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

View File

@@ -953,7 +953,7 @@ impl CommandApi {
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
}
/// Create a new group chat.
/// Create a new encrypted group chat (with key-contacts).
///
/// After creation,
/// the group has one member with the ID DC_CONTACT_ID_SELF
@@ -971,14 +971,24 @@ impl CommandApi {
///
/// @param protect If set to 1 the function creates group with protection initially enabled.
/// Only verified members are allowed in these groups
/// and end-to-end-encryption is always enabled.
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let protect = match protect {
true => ProtectionStatus::Protected,
false => ProtectionStatus::Unprotected,
};
chat::create_group_chat(&ctx, protect, &name)
chat::create_group_ex(&ctx, Some(protect), &name)
.await
.map(|id| id.to_u32())
}
/// Create a new unencrypted group chat.
///
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
/// address-contacts.
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_group_ex(&ctx, None, &name)
.await
.map(|id| id.to_u32())
}

View File

@@ -23,6 +23,7 @@ pub enum ChatListItemFetchResult {
name: String,
avatar_path: Option<String>,
color: String,
chat_type: u32,
last_updated: Option<i64>,
summary_text1: String,
summary_text2: String,
@@ -54,6 +55,7 @@ pub enum ChatListItemFetchResult {
///
/// See also `is_key_contact` on `Contact`.
is_encrypted: bool,
/// deprecated 2025-07, use chat_type instead
is_group: bool,
fresh_message_counter: usize,
is_self_talk: bool,
@@ -64,10 +66,6 @@ pub enum ChatListItemFetchResult {
is_pinned: bool,
is_muted: bool,
is_contact_request: bool,
/// Deprecated 2025-07, alias for is_out_broadcast
is_broadcast: bool,
/// true if the chat type is OutBroadcast
is_out_broadcast: bool,
/// contact id if this is a dm chat (for view profile entry in context menu)
dm_chat_contact: Option<u32>,
was_seen_recently: bool,
@@ -157,6 +155,7 @@ pub(crate) async fn get_chat_list_item_by_id(
name: chat.get_name().to_owned(),
avatar_path,
color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
last_updated,
summary_text1,
summary_text2,
@@ -174,8 +173,6 @@ pub(crate) async fn get_chat_list_item_by_id(
is_pinned: visibility == ChatVisibility::Pinned,
is_muted: chat.is_muted(),
is_contact_request: chat.is_contact_request(),
is_broadcast: chat.get_type() == Chattype::OutBroadcast,
is_out_broadcast: chat.get_type() == Chattype::OutBroadcast,
dm_chat_contact,
was_seen_recently,
last_message_type: message_type,

View File

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

View File

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

View File

@@ -87,7 +87,7 @@ async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
let data = read_file(context, filename).await?;
if let Err(err) = receive_imf(context, &data, false).await {
println!("receive_imf errored: {err:?}");
eprintln!("receive_imf errored: {err:?}");
}
Ok(())
}
@@ -621,7 +621,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Location streaming enabled.");
}
println!("{cnt} chats");
println!("{time_needed:?} to create this list");
eprintln!("{time_needed:?} to create this list");
}
"start-realtime" => {
if arg1.is_empty() {
@@ -731,7 +731,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
println!(
eprintln!(
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
);
}
@@ -985,7 +985,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
query,
);
println!("{time_needed:?} to create this list");
eprintln!("{time_needed:?} to create this list");
}
"draft" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -1151,7 +1151,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"listcontacts" | "contacts" => {
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
log_contactlist(&context, &contacts).await?;
println!("{} contacts.", contacts.len());
println!("{} key contacts.", contacts.len());
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
log_contactlist(&context, &addrcontacts).await?;
println!("{} address contacts.", addrcontacts.len());
}
"addcontact" => {
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
@@ -1224,7 +1227,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(&context, arg1).await {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => println!("Cannot set config from QR code: {err:?}"),
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
}
}
"createqrsvg" => {

View File

@@ -311,7 +311,7 @@ impl Validator for DcHelper {}
async fn start(args: Vec<String>) -> Result<(), Error> {
if args.len() < 2 {
println!("Error: Bad arguments, expected [db-name].");
eprintln!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = ContextBuilder::new(args[1].clone().into())
@@ -366,7 +366,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
false
}
Err(err) => {
println!("Error: {err:#}");
eprintln!("Error: {err:#}");
true
}
}
@@ -381,7 +381,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
break;
}
Err(err) => {
println!("Error: {err:#}");
eprintln!("Error: {err:#}");
break;
}
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.1.0"
version = "2.2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.1.0"
version = "2.2.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-07-11
2025-07-14

View File

@@ -33,6 +33,7 @@ use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
use crate::events::EventType;
use crate::location;
use crate::log::{LogExt, error, info, warn};
use crate::logged_debug_assert;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -1339,14 +1340,18 @@ impl ChatId {
let mut ret = stock_str::e2e_available(context).await + "\n";
for contact_id in get_chat_contacts(context, self)
for &contact_id in get_chat_contacts(context, self)
.await?
.iter()
.filter(|&contact_id| !contact_id.is_special())
{
let contact = Contact::get_by_id(context, *contact_id).await?;
let contact = Contact::get_by_id(context, contact_id).await?;
let addr = contact.get_addr();
debug_assert!(contact.is_key_contact());
logged_debug_assert!(
context,
contact.is_key_contact(),
"get_encryption_info: contact {contact_id} is not a key-contact."
);
let fingerprint = contact
.fingerprint()
.context("Contact does not have a fingerprint in encrypted chat")?;
@@ -2962,6 +2967,9 @@ async fn prepare_send_msg(
let row_ids = create_send_msg_jobs(context, msg)
.await
.context("Failed to create send jobs")?;
if !row_ids.is_empty() {
donation_request_maybe(context).await.log_err(context).ok();
}
Ok(row_ids)
}
@@ -3206,6 +3214,31 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
send_msg(context, chat_id, &mut msg).await
}
async fn donation_request_maybe(context: &Context) -> Result<()> {
let secs_between_checks = 30 * 24 * 60 * 60;
let now = time();
let ts = context
.get_config_i64(Config::DonationRequestNextCheck)
.await?;
if ts > now {
return Ok(());
}
let msg_cnt = context.sql.count(
"SELECT COUNT(*) FROM msgs WHERE state>=? AND hidden=0",
(MessageState::OutDelivered,),
);
let ts = if ts == 0 || msg_cnt.await? < 100 {
now.saturating_add(secs_between_checks)
} else {
let mut msg = Message::new_text(stock_str::donation_request(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
i64::MAX
};
context
.set_config_internal(Config::DonationRequestNextCheck, Some(&ts.to_string()))
.await
}
/// Chat message list request options.
#[derive(Debug)]
pub struct MessageListOptions {
@@ -3624,15 +3657,31 @@ pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Resul
}
/// Creates a group chat with a given `name`.
/// Deprecated on 2025-06-21, use `create_group_ex()`.
pub async fn create_group_chat(
context: &Context,
protect: ProtectionStatus,
chat_name: &str,
name: &str,
) -> Result<ChatId> {
let chat_name = sanitize_single_line(chat_name);
create_group_ex(context, Some(protect), name).await
}
/// Creates a group chat.
///
/// * `encryption` - If `Some`, the chat is encrypted (with key-contacts) and can be protected.
/// * `name` - Chat name.
pub async fn create_group_ex(
context: &Context,
encryption: Option<ProtectionStatus>,
name: &str,
) -> Result<ChatId> {
let chat_name = sanitize_single_line(name);
ensure!(!chat_name.is_empty(), "Invalid chat name");
let grpid = create_id();
let grpid = match encryption {
Some(_) => create_id(),
None => String::new(),
};
let timestamp = create_smeared_timestamp(context);
let row_id = context
@@ -3652,7 +3701,8 @@ pub async fn create_group_chat(
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, chat_id);
if protect == ProtectionStatus::Protected {
if encryption == Some(ProtectionStatus::Protected) {
let protect = ProtectionStatus::Protected;
chat_id
.set_protection_for_timestamp_sort(context, protect, timestamp, None)
.await?;

View File

@@ -4698,6 +4698,32 @@ async fn test_no_key_contacts_in_adhoc_chats() -> Result<()> {
Ok(())
}
/// Tests that key-contacts cannot be added to an unencrypted (ad hoc) group and the group and
/// messages report that they are unencrypted.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_unencrypted_group_chat() -> Result<()> {
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_ex(alice, None, "Group chat").await?;
let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await;
let charlie_address_contact_id = alice.add_or_lookup_address_contact_id(charlie).await;
let res = add_contact_to_chat(alice, chat_id, bob_key_contact_id).await;
assert!(res.is_err());
add_contact_to_chat(alice, chat_id, charlie_address_contact_id).await?;
let chat = Chat::load_from_db(alice, chat_id).await?;
assert!(!chat.is_encrypted(alice).await?);
let sent_msg = alice.send_text(chat_id, "Hello").await;
let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?;
assert!(!msg.get_showpadlock());
Ok(())
}
/// Tests that avatar cannot be set in ad hoc groups.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_avatar_in_adhoc_chats() -> Result<()> {

View File

@@ -369,6 +369,9 @@ pub enum Config {
#[strum(props(default = "0"))]
DisableIdle,
/// Timestamp of the next check for donation request need.
DonationRequestNextCheck,
/// Defines the max. size (in bytes) of messages downloaded automatically.
/// 0 = no limit.
#[strum(props(default = "0"))]

View File

@@ -27,6 +27,7 @@ use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_secret_key, self_fingerprint};
use crate::log::{info, warn};
use crate::logged_debug_assert;
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId};
use crate::param::{Param, Params};
@@ -660,8 +661,16 @@ impl Context {
/// or [`Self::emit_msgs_changed_without_msg_id`] should be used
/// instead of this function.
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
debug_assert!(!chat_id.is_unset());
debug_assert!(!msg_id.is_unset());
logged_debug_assert!(
self,
!chat_id.is_unset(),
"emit_msgs_changed: chat_id is unset."
);
logged_debug_assert!(
self,
!msg_id.is_unset(),
"emit_msgs_changed: msg_id is unset."
);
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
chatlist_events::emit_chatlist_changed(self);
@@ -670,7 +679,11 @@ impl Context {
/// Emits a MsgsChanged event with specified chat and without message id.
pub fn emit_msgs_changed_without_msg_id(&self, chat_id: ChatId) {
debug_assert!(!chat_id.is_unset());
logged_debug_assert!(
self,
!chat_id.is_unset(),
"emit_msgs_changed_without_msg_id: chat_id is unset."
);
self.emit_event(EventType::MsgsChanged {
chat_id,
@@ -1041,6 +1054,12 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"donation_request_next_check",
self.get_config_i64(Config::DonationRequestNextCheck)
.await?
.to_string(),
);
res.insert(
"first_key_contacts_msg_id",
self.sql

View File

@@ -178,7 +178,8 @@ mod tests {
let bob = TestContext::new_bob().await;
receive_imf(&bob, attachment_mime, false).await?;
let msg = bob.get_last_msg().await;
assert_eq!(msg.text, "Hello from Thunderbird!");
// Subject should be prepended because the attachment doesn't have "Chat-Version".
assert_eq!(msg.text, "Hello, Bob! Hello from Thunderbird!");
Ok(())
}

View File

@@ -213,17 +213,18 @@ impl Session {
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid);
let (last_uid, _received) = self
.fetch_many_msgs(
context,
folder,
uidvalidity,
vec![uid],
&uid_message_ids,
false,
)
.await?;
if last_uid.is_none() {
let (sender, receiver) = async_channel::unbounded();
self.fetch_many_msgs(
context,
folder,
uidvalidity,
vec![uid],
&uid_message_ids,
false,
sender,
)
.await?;
if receiver.recv().await.is_err() {
bail!("Failed to fetch UID {uid}");
}
Ok(())

View File

@@ -14,7 +14,7 @@ use std::{
};
use anyhow::{Context as _, Result, bail, ensure, format_err};
use async_channel::Receiver;
use async_channel::{self, Receiver, Sender};
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
use deltachat_contact_tools::ContactAddress;
use futures::{FutureExt as _, StreamExt, TryStreamExt};
@@ -562,7 +562,7 @@ impl Imap {
let read_cnt = msgs.len();
let download_limit = context.download_limit().await?;
let mut uids_fetch = Vec::<(_, bool /* partially? */)>::with_capacity(msgs.len() + 1);
let mut uids_fetch = Vec::<(u32, bool /* partially? */)>::with_capacity(msgs.len() + 1);
let mut uid_message_ids = BTreeMap::new();
let mut largest_uid_skipped = None;
let delete_target = context.get_delete_msgs_target().await?;
@@ -695,51 +695,72 @@ impl Imap {
self.connectivity.set_working(context).await;
}
// Actually download messages.
let mut largest_uid_fetched: u32 = 0;
let mut received_msgs = Vec::with_capacity(uids_fetch.len());
let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
let mut fetch_partially = false;
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
for (uid, fp) in uids_fetch {
if fp != fetch_partially {
let (largest_uid_fetched_in_batch, received_msgs_in_batch) = session
.fetch_many_msgs(
context,
folder,
uid_validity,
uids_fetch_in_batch.split_off(0),
&uid_message_ids,
fetch_partially,
)
.await
.context("fetch_many_msgs")?;
received_msgs.extend(received_msgs_in_batch);
largest_uid_fetched = max(
largest_uid_fetched,
largest_uid_fetched_in_batch.unwrap_or(0),
);
fetch_partially = fp;
}
uids_fetch_in_batch.push(uid);
}
let (sender, receiver) = async_channel::unbounded();
// Advance uid_next to the maximum of the largest known UID plus 1
// and mailbox UIDNEXT.
// Largest known UID is normally less than UIDNEXT,
// but a message may have arrived between determining UIDNEXT
// and executing the FETCH command.
let mut received_msgs = Vec::with_capacity(uids_fetch.len());
let mailbox_uid_next = session
.selected_mailbox
.as_ref()
.with_context(|| format!("Expected {folder:?} to be selected"))?
.uid_next
.unwrap_or_default();
let new_uid_next = max(
max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0)) + 1,
mailbox_uid_next,
);
let update_uids_future = async {
let mut largest_uid_fetched: u32 = 0;
while let Ok((uid, received_msg_opt)) = receiver.recv().await {
largest_uid_fetched = max(largest_uid_fetched, uid);
if let Some(received_msg) = received_msg_opt {
received_msgs.push(received_msg)
}
}
largest_uid_fetched
};
let actually_download_messages_future = async move {
let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
let mut fetch_partially = false;
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
for (uid, fp) in uids_fetch {
if fp != fetch_partially {
session
.fetch_many_msgs(
context,
folder,
uid_validity,
uids_fetch_in_batch.split_off(0),
&uid_message_ids,
fetch_partially,
sender.clone(),
)
.await
.context("fetch_many_msgs")?;
fetch_partially = fp;
}
uids_fetch_in_batch.push(uid);
}
anyhow::Ok(())
};
let (largest_uid_fetched, fetch_res) =
tokio::join!(update_uids_future, actually_download_messages_future);
// Advance uid_next to the largest fetched UID plus 1.
//
// This may be larger than `mailbox_uid_next`
// if the message has arrived after selecting mailbox
// and determining its UIDNEXT and before prefetch.
let mut new_uid_next = largest_uid_fetched + 1;
if fetch_res.is_ok() {
// If we have successfully fetched all messages we planned during prefetch,
// then we have covered at least the range between old UIDNEXT
// and UIDNEXT of the mailbox at the time of selecting it.
new_uid_next = max(new_uid_next, mailbox_uid_next);
new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
}
if new_uid_next > old_uid_next {
set_uid_next(context, folder, new_uid_next).await?;
}
@@ -752,6 +773,10 @@ impl Imap {
chat::mark_old_messages_as_noticed(context, received_msgs).await?;
// Now fail if fetching failed, so we will
// establish a new session if this one is broken.
fetch_res?;
Ok(read_cnt > 0)
}
@@ -1300,9 +1325,19 @@ impl Session {
/// Fetches a list of messages by server UID.
///
/// Returns the last UID fetched successfully and the info about each downloaded message.
/// Sends pairs of UID and info about each downloaded message to the provided channel.
/// Received message info is optional because UID may be ignored
/// if the message has a `\Deleted` flag.
///
/// The channel is used to return the results because the function may fail
/// due to network errors before it finishes fetching all the messages.
/// In this case caller still may want to process all the results
/// received over the channel and persist last seen UID in the database
/// before bubbling up the failure.
///
/// If the message is incorrect or there is a failure to write a message to the database,
/// it is skipped and the error is logged.
#[expect(clippy::too_many_arguments)]
pub(crate) async fn fetch_many_msgs(
&mut self,
context: &Context,
@@ -1311,12 +1346,10 @@ impl Session {
request_uids: Vec<u32>,
uid_message_ids: &BTreeMap<u32, String>,
fetch_partially: bool,
) -> Result<(Option<u32>, Vec<ReceivedMsg>)> {
let mut last_uid = None;
let mut received_msgs = Vec::new();
received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
) -> Result<()> {
if request_uids.is_empty() {
return Ok((last_uid, received_msgs));
return Ok(());
}
for (request_uids, set) in build_sequence_sets(&request_uids)? {
@@ -1402,7 +1435,7 @@ impl Session {
if is_deleted {
info!(context, "Not processing deleted msg {}.", request_uid);
last_uid = Some(request_uid);
received_msgs_channel.send((request_uid, None)).await?;
continue;
}
@@ -1413,7 +1446,7 @@ impl Session {
context,
"Not processing message {} without a BODY.", request_uid
);
last_uid = Some(request_uid);
received_msgs_channel.send((request_uid, None)).await?;
continue;
};
@@ -1445,15 +1478,15 @@ impl Session {
.await
{
Ok(received_msg) => {
if let Some(m) = received_msg {
received_msgs.push(m);
}
received_msgs_channel
.send((request_uid, received_msg))
.await?;
}
Err(err) => {
warn!(context, "receive_imf error: {:#}.", err);
received_msgs_channel.send((request_uid, None)).await?;
}
};
last_uid = Some(request_uid)
}
// If we don't process the whole response, IMAP client is left in a broken state where
@@ -1477,7 +1510,7 @@ impl Session {
}
}
Ok((last_uid, received_msgs))
Ok(())
}
/// Retrieves server metadata if it is supported.

View File

@@ -1,3 +1,4 @@
use crate::headerdef::HeaderDef;
use num_traits::FromPrimitive;
use super::*;
@@ -805,3 +806,24 @@ async fn test_sanitize_filename_message() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_autocrypt_gossip_chat_assign() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = alice.create_chat(bob).await.id;
alice.set_config_u32(Config::GossipPeriod, 0).await?;
tcm.section("Bob receives a normal message from alice, and assigns it a chat");
let sent_msg = alice.send_text(alice_chat_id, "hi").await;
let bob_chat_id = bob.recv_msg(&sent_msg).await.chat_id;
tcm.section("Bob receives a message with a gossip header and assigns it to the same chat");
let sent_msg = alice.send_text(alice_chat_id, "hi").await;
let msg = bob.parse_msg(&sent_msg).await;
assert!(msg.header_exists(HeaderDef::AutocryptGossip));
assert_eq!(bob.recv_msg(&sent_msg).await.chat_id, bob_chat_id);
Ok(())
}

View File

@@ -368,8 +368,14 @@ impl MimeFactory {
}
}
ensure_and_debug_assert!(member_timestamps.len() >= to.len());
ensure_and_debug_assert!(member_fingerprints.is_empty() || member_fingerprints.len() >= to.len());
ensure_and_debug_assert!(
member_timestamps.len() >= to.len(),
"member_timestamps.len() ({}) < to.len() ({})",
member_timestamps.len(), to.len());
ensure_and_debug_assert!(
member_fingerprints.is_empty() || member_fingerprints.len() >= to.len(),
"member_fingerprints.len() ({}) < to.len() ({})",
member_fingerprints.len(), to.len());
if to.len() > 1 {
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
@@ -448,7 +454,11 @@ impl MimeFactory {
ensure_and_debug_assert!(
member_timestamps.is_empty()
|| to.len() + past_members.len() == member_timestamps.len()
|| to.len() + past_members.len() == member_timestamps.len(),
"to.len() ({}) + past_members.len() ({}) != member_timestamps.len() ({})",
to.len(),
past_members.len(),
member_timestamps.len(),
);
let factory = MimeFactory {
@@ -671,7 +681,11 @@ impl MimeFactory {
ensure_and_debug_assert!(
self.member_timestamps.is_empty()
|| to.len() + past_members.len() == self.member_timestamps.len()
|| to.len() + past_members.len() == self.member_timestamps.len(),
"to.len() ({}) + past_members.len() ({}) != self.member_timestamps.len() ({})",
to.len(),
past_members.len(),
self.member_timestamps.len(),
);
if to.is_empty() {
to.push(hidden_recipients());

View File

@@ -246,6 +246,7 @@ impl MimeMessage {
MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut from,
@@ -273,6 +274,7 @@ impl MimeMessage {
MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut from,
@@ -446,26 +448,11 @@ impl MimeMessage {
});
if let (Ok(mail), true) = (mail, is_encrypted) {
if !signatures.is_empty() {
// Remove unsigned opportunistically protected headers from messages considered
// Autocrypt-encrypted / displayed with padlock.
// For "Subject" see <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
for h in [
HeaderDef::Subject,
HeaderDef::ChatGroupId,
HeaderDef::ChatGroupName,
HeaderDef::ChatGroupNameChanged,
HeaderDef::ChatGroupNameTimestamp,
HeaderDef::ChatGroupAvatar,
HeaderDef::ChatGroupMemberRemoved,
HeaderDef::ChatGroupMemberAdded,
HeaderDef::ChatGroupMemberTimestamps,
HeaderDef::ChatGroupPastMembers,
HeaderDef::ChatDelete,
HeaderDef::ChatEdit,
HeaderDef::ChatUserAvatar,
] {
remove_header(&mut headers, h.get_headername(), &mut headers_removed);
}
// Unsigned "Subject" mustn't be prepended to messages shown as encrypted
// (<https://github.com/deltachat/deltachat-core-rust/issues/1790>).
// Other headers are removed by `MimeMessage::merge_headers()` except for "List-ID".
remove_header(&mut headers, "subject", &mut headers_removed);
remove_header(&mut headers, "list-id", &mut headers_removed);
}
// let known protected headers from the decrypted
@@ -478,6 +465,7 @@ impl MimeMessage {
MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut inner_from,
@@ -1558,6 +1546,7 @@ impl MimeMessage {
fn merge_headers(
context: &Context,
headers: &mut HashMap<String, String>,
headers_removed: &mut HashSet<String>,
recipients: &mut Vec<SingleInfo>,
past_members: &mut Vec<SingleInfo>,
from: &mut Option<SingleInfo>,
@@ -1565,23 +1554,25 @@ impl MimeMessage {
chat_disposition_notification_to: &mut Option<SingleInfo>,
fields: &[mailparse::MailHeader<'_>],
) {
headers.retain(|k, _| {
!is_protected(k) || {
headers_removed.insert(k.to_string());
false
}
});
for field in fields {
// lowercasing all headers is technically not correct, but makes things work better
let key = field.get_key().to_lowercase();
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
is_known(&key) || key.starts_with("chat-")
{
if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
match addrparse_header(field) {
Ok(addrlist) => {
*chat_disposition_notification_to = addrlist.extract_single_info();
}
Err(e) => warn!(context, "Could not read {} address: {}", key, e),
if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
match addrparse_header(field) {
Ok(addrlist) => {
*chat_disposition_notification_to = addrlist.extract_single_info();
}
} else {
let value = field.get_value();
headers.insert(key.to_string(), value);
Err(e) => warn!(context, "Could not read {} address: {}", key, e),
}
} else {
let value = field.get_value();
headers.insert(key.to_string(), value);
}
}
let recipients_new = get_recipients(fields);
@@ -2009,26 +2000,30 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
}
}
/// Returns true if the header overwrites outer header
/// when it comes from protected headers.
fn is_known(key: &str) -> bool {
matches!(
key,
"return-path"
| "date"
| "from"
| "sender"
| "reply-to"
| "to"
| "cc"
| "bcc"
| "message-id"
| "in-reply-to"
| "references"
| "subject"
| "secure-join"
| "list-id"
)
/// Returns whether the outer header value must be ignored if the message contains a signed (and
/// optionally encrypted) part.
///
/// NB: There are known cases when Subject and List-ID only appear in the outer headers of
/// signed-only messages. Such messages are shown as unencrypted anyway.
fn is_protected(key: &str) -> bool {
key.starts_with("chat-")
|| matches!(
key,
"return-path"
| "auto-submitted"
| "autocrypt-setup-message"
| "date"
| "from"
| "sender"
| "reply-to"
| "to"
| "cc"
| "bcc"
| "message-id"
| "in-reply-to"
| "references"
| "secure-join"
)
}
/// Returns if the header is hidden and must be ignored in the IMF section.

View File

@@ -1402,6 +1402,26 @@ async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_extra_imf_chat_header() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let chat_id = t.get_self_chat().await.id;
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
let sent_msg = t.pop_sent_msg().await;
// Check removal of some nonexistent "Chat-*" header to protect the code from future breakages.
let payload = sent_msg
.payload
.replace("Message-ID:", "Chat-Forty-Two: 42\r\nMessage-ID:");
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None)
.await
.unwrap();
assert!(msg.headers.contains_key("chat-version"));
assert!(!msg.headers.contains_key("chat-forty-two"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_long_in_reply_to() -> Result<()> {
let t = TestContext::new_alice().await;

View File

@@ -31,6 +31,7 @@ use crate::key::self_fingerprint_opt;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::log::LogExt;
use crate::log::{info, warn};
use crate::logged_debug_assert;
use crate::message::{
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
};
@@ -1456,7 +1457,10 @@ async fn do_chat_assignment(
false => None,
};
if let Some(chat) = chat {
ensure_and_debug_assert!(chat.typ == Chattype::Single);
ensure_and_debug_assert!(
chat.typ == Chattype::Single,
"Chat {chat_id} is not Single",
);
let mut new_protection = match verified_encryption {
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
@@ -2141,7 +2145,7 @@ RETURNING id
// afterwards insert additional parts.
replace_msg_id = None;
ensure_and_debug_assert!(!row_id.is_special());
ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
created_db_entries.push(row_id);
}
@@ -2405,7 +2409,9 @@ async fn lookup_chat_by_reply(
// as we can directly assign the message to the chat
// by its group ID.
ensure_and_debug_assert!(
mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted()
mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
"Encrypted message has group ID {}",
mime_parser.get_chat_group_id().unwrap_or_default(),
);
// Try to assign message to the same chat as the parent message.
@@ -3835,7 +3841,11 @@ async fn lookup_key_contact_by_fingerprint(
context: &Context,
fingerprint: &str,
) -> Result<Option<ContactId>> {
debug_assert!(!fingerprint.is_empty());
logged_debug_assert!(
context,
!fingerprint.is_empty(),
"lookup_key_contact_by_fingerprint: fingerprint is empty."
);
if fingerprint.is_empty() {
// Avoid accidentally looking up a non-key-contact.
return Ok(None);

View File

@@ -3682,6 +3682,24 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ignore_protected_headers_in_outer_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
send_text_msg(bob, bob_chat_id, "hi all!".to_string()).await?;
let mut sent_msg = bob.pop_sent_msg().await;
sent_msg.payload = sent_msg.payload.replace(
"Chat-Version:",
"Auto-Submitted: auto-generated\r\nChat-Version:",
);
alice.recv_msg(&sent_msg).await;
let ab_contact = alice.add_or_lookup_contact(bob).await;
assert!(!ab_contact.is_bot());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_member_list_on_rejoin() -> Result<()> {
let mut tcm = TestContextManager::new();

View File

@@ -16,6 +16,7 @@ use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key::{DcKey, Fingerprint, load_self_public_key};
use crate::log::{error, info, warn};
use crate::logged_debug_assert;
use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
@@ -32,9 +33,10 @@ use qrinvite::QrInvite;
use crate::token::Namespace;
fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
debug_assert!(
logged_debug_assert!(
context,
progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
"inviter_progress: contact {contact_id}, progress={progress}, but value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success."
);
context.emit_event(EventType::SecurejoinInviterProgress {
contact_id,

View File

@@ -413,6 +413,16 @@ pub enum StockMessage {
#[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
SecurejoinWait = 190,
#[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
Please consider donating to help that Delta Chat stays free for everyone.
While Delta Chat is free to use and open source, development costs money.
Help keeping us to keep Delta Chat independent and make it more awesome in the future.
https://delta.chat/donate"))]
DonationRequest = 193,
}
impl StockMessage {
@@ -785,6 +795,11 @@ pub(crate) async fn securejoin_wait(context: &Context) -> String {
translated(context, StockMessage::SecurejoinWait).await
}
/// Stock string: `❤️ Seems you're enjoying Delta Chat!`…
pub(crate) async fn donation_request(context: &Context) -> String {
translated(context, StockMessage::DonationRequest).await
}
/// Stock string: `Scan to chat with %1$s`.
pub(crate) async fn setup_contact_qr_description(
context: &Context,

View File

@@ -754,7 +754,7 @@ impl TestContext {
pub async fn add_or_lookup_address_contact(&self, other: &TestContext) -> Contact {
let contact_id = self.add_or_lookup_address_contact_id(other).await;
let contact = Contact::get_by_id(&self.ctx, contact_id).await.unwrap();
debug_assert_eq!(contact.is_key_contact(), false);
assert_eq!(contact.is_key_contact(), false);
contact
}
@@ -1082,8 +1082,6 @@ impl Drop for TestContext {
.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")
}
});
}
@@ -1168,6 +1166,11 @@ impl Drop for InnerLogSink {
while let Ok(event) = self.events.try_recv() {
print_logevent(&event);
}
if std::env::var("DELTACHAT_SAVE_TMP_DB").is_err() {
eprintln!(
"note: If you want to examine the database files, set environment variable DELTACHAT_SAVE_TMP_DB=1"
)
}
}
}

View File

@@ -767,9 +767,10 @@ pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
/// In non-optimized builds, panics instead if so.
#[macro_export]
macro_rules! ensure_and_debug_assert {
($($arg:tt)*) => {
debug_assert!($($arg)*);
anyhow::ensure!($($arg)*);
($cond:expr, $($arg:tt)*) => {
let cond_val = $cond;
debug_assert!(cond_val, $($arg)*);
anyhow::ensure!(cond_val, $($arg)*);
};
}
@@ -801,5 +802,18 @@ macro_rules! ensure_and_debug_assert_ne {
};
}
/// Logs a warning if a condition is not satisfied.
/// In non-optimized builds, panics also if so.
#[macro_export]
macro_rules! logged_debug_assert {
($ctx:expr, $cond:expr, $($arg:tt)*) => {
let cond_val = $cond;
if !cond_val {
warn!($ctx, $($arg)*);
}
debug_assert!(cond_val, $($arg)*);
};
}
#[cfg(test)]
mod tools_tests;