mirror of
https://github.com/chatmail/core.git
synced 2026-05-09 09:56:31 +03:00
Compare commits
9 Commits
hoc/remove
...
hoc/dont-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f94731475 | ||
|
|
67a75a432c | ||
|
|
d014255bfe | ||
|
|
537ac50690 | ||
|
|
e98c6b4fd3 | ||
|
|
893ad06a61 | ||
|
|
25ac5a2363 | ||
|
|
4d537544ef | ||
|
|
4a16c0c3dd |
@@ -348,7 +348,7 @@ def test_receive_imf_failure(acfactory) -> None:
|
||||
snapshot.text == "❌ Failed to receive a message:"
|
||||
" Condition failed: `!context.get_config_bool(Config::SimulateReceiveImfError).await?`."
|
||||
f" Core version {version}."
|
||||
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
|
||||
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/"
|
||||
)
|
||||
|
||||
# The failed message doesn't break the IMAP loop.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Helper functions for decryption.
|
||||
//! The actual decryption is done in the [`crate::pgp`] module.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
@@ -18,8 +19,8 @@ use crate::chat::ChatId;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::key::load_self_secret_keyring;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::key::{Fingerprint, SignedPublicKey, load_self_secret_keyring};
|
||||
use crate::token::Namespace;
|
||||
|
||||
/// Tries to decrypt the message,
|
||||
@@ -334,6 +335,36 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
|
||||
///
|
||||
/// Returns the signed part and the set of key
|
||||
/// fingerprints for which there is a valid signature.
|
||||
///
|
||||
/// Returns None if the message is not Multipart/Signed or doesn't contain necessary parts.
|
||||
pub(crate) fn validate_detached_signature<'a, 'b>(
|
||||
mail: &'a ParsedMail<'b>,
|
||||
public_keyring_for_validate: &[SignedPublicKey],
|
||||
) -> Option<(&'a ParsedMail<'b>, HashSet<Fingerprint>)> {
|
||||
if mail.ctype.mimetype != "multipart/signed" {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
// First part is the content, second part is the signature.
|
||||
let content = first_part.raw_bytes;
|
||||
let ret_valid_signatures = match second_part.get_body_raw() {
|
||||
Ok(signature) => {
|
||||
crate::pgp::pk_validate(content, &signature, public_keyring_for_validate)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Err(_) => Default::default(),
|
||||
};
|
||||
Some((first_part, ret_valid_signatures))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
16
src/imap.rs
16
src/imap.rs
@@ -1383,13 +1383,15 @@ impl Session {
|
||||
let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
|
||||
let received_msg = match res {
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf error: {err:#}.");
|
||||
|
||||
let text = format!(
|
||||
"❌ Failed to receive a message: {err:#}. Core version v{DC_VERSION_STR}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/.",
|
||||
);
|
||||
let mut msg = Message::new_text(text);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
let err = format!("{err:#}");
|
||||
warn!(context, "receive_imf error: {err}.");
|
||||
if !err.contains("(SKIP_DEVICE_MSG)") {
|
||||
let text = format!(
|
||||
"❌ Failed to receive a message: {err}. Core version v{DC_VERSION_STR}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/",
|
||||
);
|
||||
let mut msg = Message::new_text(text);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
None
|
||||
}
|
||||
Ok(msg) => msg,
|
||||
|
||||
@@ -871,7 +871,7 @@ mod tests {
|
||||
use crate::config::Config;
|
||||
use crate::message::MessageState;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::test_utils::{ExpectedEvents, TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
#[test]
|
||||
@@ -1103,6 +1103,9 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
.await?;
|
||||
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
// Bob needs the chat accepted so that "normal" messages from Alice trigger `IncomingMsg`.
|
||||
// Location-only messages still must trigger `MsgsChanged`.
|
||||
bob.create_chat(alice).await;
|
||||
|
||||
// Alice enables location streaming.
|
||||
// Bob receives a message saying that Alice enabled location streaming.
|
||||
@@ -1117,7 +1120,18 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
SystemTime::shift(Duration::from_secs(10));
|
||||
delete_expired(alice, time()).await?;
|
||||
maybe_send(alice).await?;
|
||||
bob.evtracker.clear_events();
|
||||
bob.recv_msg_opt(&alice.pop_sent_msg().await).await;
|
||||
bob.evtracker
|
||||
.get_matching_ex(
|
||||
bob,
|
||||
ExpectedEvents {
|
||||
expected: |e| matches!(e, EventType::MsgsChanged { .. }),
|
||||
unexpected: |e| matches!(e, EventType::IncomingMsg { .. }),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
|
||||
assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::{ContactId, import_public_key};
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::{self};
|
||||
use crate::decrypt::{self, validate_detached_signature};
|
||||
use crate::dehtml::dehtml;
|
||||
use crate::download::PostMsgMetadata;
|
||||
use crate::events::EventType;
|
||||
@@ -487,6 +487,17 @@ impl MimeMessage {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
let mail = mail.as_ref().map(|mail| {
|
||||
let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
|
||||
.unwrap_or((mail, Default::default()));
|
||||
let signatures_detached = signatures_detached
|
||||
.into_iter()
|
||||
.map(|fp| (fp, Vec::new()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
signatures.extend(signatures_detached);
|
||||
content
|
||||
});
|
||||
|
||||
if let Some(expected_sender_fingerprint) = expected_sender_fingerprint {
|
||||
ensure!(
|
||||
!signatures.is_empty(),
|
||||
@@ -502,7 +513,7 @@ impl MimeMessage {
|
||||
);
|
||||
}
|
||||
|
||||
if let (Ok(mail), true) = (&mail, is_encrypted) {
|
||||
if let (Ok(mail), true) = (mail, is_encrypted) {
|
||||
if !signatures.is_empty() {
|
||||
// Unsigned "Subject" mustn't be prepended to messages shown as encrypted
|
||||
// (<https://github.com/deltachat/deltachat-core-rust/issues/1790>).
|
||||
@@ -527,7 +538,7 @@ impl MimeMessage {
|
||||
&mut inner_from,
|
||||
&mut list_post,
|
||||
&mut chat_disposition_notification_to,
|
||||
&mail,
|
||||
mail,
|
||||
);
|
||||
|
||||
if !signatures.is_empty() {
|
||||
@@ -571,7 +582,7 @@ impl MimeMessage {
|
||||
signatures.clear();
|
||||
}
|
||||
|
||||
if let (Ok(mail), true) = (&mail, is_encrypted)
|
||||
if let (Ok(mail), true) = (mail, is_encrypted)
|
||||
&& let Some(post_msg_rfc724_mid) =
|
||||
mail.headers.get_header_value(HeaderDef::ChatPostMessageId)
|
||||
{
|
||||
@@ -629,7 +640,7 @@ impl MimeMessage {
|
||||
from,
|
||||
incoming,
|
||||
chat_disposition_notification_to,
|
||||
decryption_error: mail.as_ref().err().map(|err| format!("{err:#}")),
|
||||
decryption_error: mail.err().map(|err| format!("{err:#}")),
|
||||
|
||||
// only non-empty if it was a valid autocrypt message
|
||||
signature,
|
||||
@@ -655,9 +666,9 @@ impl MimeMessage {
|
||||
pre_message,
|
||||
};
|
||||
|
||||
match &mail {
|
||||
match mail {
|
||||
Ok(mail) => {
|
||||
parser.parse_mime_recursive(context, &mail, false).await?;
|
||||
parser.parse_mime_recursive(context, mail, false).await?;
|
||||
}
|
||||
Err(err) => {
|
||||
let txt = "[This message cannot be decrypted.\n\n• It might already help to simply reply to this message and ask the sender to send the message again.\n\n• If you just re-installed Delta Chat then it is best if you re-setup Delta Chat now and choose \"Add as second device\" or import a backup.]";
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{Chat, ChatId, send_msg};
|
||||
@@ -243,7 +243,7 @@ pub(crate) async fn set_msg_reaction(
|
||||
timestamp: i64,
|
||||
reaction: Reaction,
|
||||
is_incoming_fresh: bool,
|
||||
) -> Result<()> {
|
||||
) -> Result<bool> {
|
||||
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?;
|
||||
|
||||
@@ -258,13 +258,14 @@ pub(crate) async fn set_msg_reaction(
|
||||
reaction,
|
||||
});
|
||||
}
|
||||
Ok(true)
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Can't assign reaction to unknown message with Message-ID {}", in_reply_to
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a structure containing all reactions to the message.
|
||||
@@ -519,6 +520,54 @@ Content-Disposition: reaction\n\
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reaction_and_multitransport() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let device_chat_id = ChatId::get_for_contact(alice, ContactId::DEVICE).await?;
|
||||
let n_device_msgs = get_chat_msgs(alice, device_chat_id).await?.len();
|
||||
|
||||
let reaction_bytes = "To: alice@example.org, claire@example.org\n\
|
||||
From: bob@example.net\n\
|
||||
Date: Today, 29 February 2021 00:00:10 -800\n\
|
||||
Message-ID: 56789@example.net\n\
|
||||
In-Reply-To: 12345@example.org\n\
|
||||
Content-Type: text/plain; charset=utf-8\n\
|
||||
Content-Disposition: reaction\n\
|
||||
\n\
|
||||
\u{1F44D}"
|
||||
.as_bytes();
|
||||
// Alice receives a reaction to Claire's message from Bob earler than the message itself
|
||||
// because Bob knows about Alice's new transport.
|
||||
receive_imf(alice, reaction_bytes, false).await?;
|
||||
|
||||
let msg_id = receive_imf(
|
||||
alice,
|
||||
"To: alice@example.org, bob@example.net\n\
|
||||
From: claire@example.org\n\
|
||||
Date: Today, 29 February 2021 00:00:00 -800\n\
|
||||
Message-ID: 12345@example.org\n\
|
||||
\n\
|
||||
Can we chat at 1pm pacific, today?"
|
||||
.as_bytes(),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap()
|
||||
.msg_ids[0];
|
||||
|
||||
// Finally the reaction arrives on Alice's older transport.
|
||||
receive_imf(alice, reaction_bytes, false).await?;
|
||||
let reactions = get_msg_reactions(alice, msg_id).await?;
|
||||
assert_eq!(reactions.to_string(), "👍1");
|
||||
|
||||
assert_eq!(
|
||||
get_chat_msgs(alice, device_chat_id).await?.len(),
|
||||
n_device_msgs
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn expect_reactions_changed_event(
|
||||
t: &TestContext,
|
||||
expected_chat_id: ChatId,
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::iter;
|
||||
use std::str::FromStr as _;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use deltachat_contact_tools::{
|
||||
ContactAddress, addr_cmp, addr_normalize, may_be_valid_addr, sanitize_bidi_characters,
|
||||
sanitize_single_line,
|
||||
@@ -935,75 +935,6 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
|
||||
// This is a Delta Chat MDN. Mark as read.
|
||||
markseen_on_imap_table(context, rfc724_mid_orig).await?;
|
||||
}
|
||||
if !mime_parser.incoming && !context.get_config_bool(Config::TeamProfile).await? {
|
||||
let mut updated_chats = BTreeMap::new();
|
||||
let mut archived_chats_maybe_noticed = false;
|
||||
for report in &mime_parser.mdn_reports {
|
||||
for msg_rfc724_mid in report
|
||||
.original_message_id
|
||||
.iter()
|
||||
.chain(&report.additional_message_ids)
|
||||
{
|
||||
let Some(msg_id) = rfc724_mid_exists(context, msg_rfc724_mid).await? else {
|
||||
continue;
|
||||
};
|
||||
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
|
||||
continue;
|
||||
};
|
||||
if msg.state < MessageState::InFresh || msg.state >= MessageState::InSeen {
|
||||
continue;
|
||||
}
|
||||
if !mime_parser.was_encrypted() && msg.get_showpadlock() {
|
||||
warn!(context, "MDN: Not encrypted. Ignoring.");
|
||||
continue;
|
||||
}
|
||||
message::update_msg_state(context, msg_id, MessageState::InSeen).await?;
|
||||
if let Err(e) = msg_id.start_ephemeral_timer(context).await {
|
||||
error!(context, "start_ephemeral_timer for {msg_id}: {e:#}.");
|
||||
}
|
||||
if !mime_parser.has_chat_version() {
|
||||
continue;
|
||||
}
|
||||
archived_chats_maybe_noticed |= msg.state < MessageState::InNoticed
|
||||
&& msg.chat_visibility == ChatVisibility::Archived;
|
||||
updated_chats
|
||||
.entry(msg.chat_id)
|
||||
.and_modify(|pos| *pos = cmp::max(*pos, (msg.timestamp_sort, msg.id)))
|
||||
.or_insert((msg.timestamp_sort, msg.id));
|
||||
}
|
||||
}
|
||||
for (chat_id, (timestamp_sort, msg_id)) in updated_chats {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"
|
||||
UPDATE msgs SET state=? WHERE
|
||||
state=? AND
|
||||
hidden=0 AND
|
||||
chat_id=? AND
|
||||
(timestamp,id)<(?,?)",
|
||||
(
|
||||
MessageState::InNoticed,
|
||||
MessageState::InFresh,
|
||||
chat_id,
|
||||
timestamp_sort,
|
||||
msg_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.context("UPDATE msgs.state")?;
|
||||
if chat_id.get_fresh_msg_cnt(context).await? == 0 {
|
||||
// Removes all notifications for the chat in UIs.
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id));
|
||||
} else {
|
||||
context.emit_msgs_changed_without_msg_id(chat_id);
|
||||
}
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
}
|
||||
if archived_chats_maybe_noticed {
|
||||
context.on_archived_chats_maybe_noticed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mime_parser.is_call() {
|
||||
@@ -1019,8 +950,15 @@ UPDATE msgs SET state=? WHERE
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
let is_pre_message = matches!(mime_parser.pre_message, PreMessageMode::Pre { .. });
|
||||
let skip_bot_notify = is_bot && is_pre_message;
|
||||
let important =
|
||||
mime_parser.incoming && fresh && !is_old_contact_request && !skip_bot_notify;
|
||||
let is_empty = !is_pre_message
|
||||
&& mime_parser.parts.first().is_none_or(|p| {
|
||||
p.typ == Viewtype::Text && p.msg.is_empty() && p.param.get(Param::Quote).is_none()
|
||||
});
|
||||
let important = mime_parser.incoming
|
||||
&& !is_empty
|
||||
&& fresh
|
||||
&& !is_old_contact_request
|
||||
&& !skip_bot_notify;
|
||||
|
||||
for msg_id in &received_msg.msg_ids {
|
||||
chat_id.emit_msg_event(context, *msg_id, important);
|
||||
@@ -2058,9 +1996,8 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot add iroh peer because WebXDC instance does not exist."
|
||||
bail!(
|
||||
"Cannot add iroh peer because WebXDC instance {in_reply_to} does not exist (SKIP_DEVICE_MSG)"
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -2093,6 +2030,82 @@ async fn add_parts(
|
||||
warn!(context, "Call: Not a reply.")
|
||||
}
|
||||
}
|
||||
if !mime_parser.incoming && !context.get_config_bool(Config::TeamProfile).await? {
|
||||
let mut missing_rfc724_mid = None;
|
||||
let mut updated_chats = BTreeMap::new();
|
||||
let mut archived_chats_maybe_noticed = false;
|
||||
for report in &mime_parser.mdn_reports {
|
||||
for msg_rfc724_mid in report
|
||||
.original_message_id
|
||||
.iter()
|
||||
.chain(&report.additional_message_ids)
|
||||
{
|
||||
let Some(msg_id) = rfc724_mid_exists(context, msg_rfc724_mid).await? else {
|
||||
missing_rfc724_mid.get_or_insert(msg_rfc724_mid.as_str());
|
||||
continue;
|
||||
};
|
||||
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
|
||||
continue;
|
||||
};
|
||||
if msg.state < MessageState::InFresh || msg.state >= MessageState::InSeen {
|
||||
continue;
|
||||
}
|
||||
if !mime_parser.was_encrypted() && msg.get_showpadlock() {
|
||||
warn!(context, "MDN: Not encrypted. Ignoring.");
|
||||
continue;
|
||||
}
|
||||
message::update_msg_state(context, msg_id, MessageState::InSeen).await?;
|
||||
if let Err(e) = msg_id.start_ephemeral_timer(context).await {
|
||||
error!(context, "start_ephemeral_timer for {msg_id}: {e:#}.");
|
||||
}
|
||||
if !mime_parser.has_chat_version() {
|
||||
continue;
|
||||
}
|
||||
archived_chats_maybe_noticed |= msg.state < MessageState::InNoticed
|
||||
&& msg.chat_visibility == ChatVisibility::Archived;
|
||||
updated_chats
|
||||
.entry(msg.chat_id)
|
||||
.and_modify(|pos| *pos = cmp::max(*pos, (msg.timestamp_sort, msg.id)))
|
||||
.or_insert((msg.timestamp_sort, msg.id));
|
||||
}
|
||||
}
|
||||
for (chat_id, (timestamp_sort, msg_id)) in updated_chats {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"
|
||||
UPDATE msgs SET state=? WHERE
|
||||
state=? AND
|
||||
hidden=0 AND
|
||||
chat_id=? AND
|
||||
(timestamp,id)<(?,?)",
|
||||
(
|
||||
MessageState::InNoticed,
|
||||
MessageState::InFresh,
|
||||
chat_id,
|
||||
timestamp_sort,
|
||||
msg_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.context("UPDATE msgs.state")?;
|
||||
if chat_id.get_fresh_msg_cnt(context).await? == 0 {
|
||||
// Removes all notifications for the chat in UIs.
|
||||
context.emit_event(EventType::MsgsNoticed(chat_id));
|
||||
} else {
|
||||
context.emit_msgs_changed_without_msg_id(chat_id);
|
||||
}
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
}
|
||||
if archived_chats_maybe_noticed {
|
||||
context.on_archived_chats_maybe_noticed();
|
||||
}
|
||||
ensure!(
|
||||
missing_rfc724_mid.is_none(),
|
||||
"Self-MDN: {} not found (SKIP_DEVICE_MSG)",
|
||||
missing_rfc724_mid.unwrap_or(""),
|
||||
);
|
||||
}
|
||||
|
||||
let hidden = mime_parser.parts.iter().all(|part| part.is_reaction);
|
||||
let mut parts = mime_parser.parts.iter().peekable();
|
||||
@@ -2101,7 +2114,7 @@ async fn add_parts(
|
||||
if part.is_reaction {
|
||||
let reaction_str = simplify::remove_footers(part.msg.as_str());
|
||||
let is_incoming_fresh = mime_parser.incoming && !seen;
|
||||
set_msg_reaction(
|
||||
if !set_msg_reaction(
|
||||
context,
|
||||
mime_in_reply_to,
|
||||
chat_id,
|
||||
@@ -2110,7 +2123,17 @@ async fn add_parts(
|
||||
Reaction::new(reaction_str.as_str()),
|
||||
is_incoming_fresh,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
{
|
||||
return Ok(ReceivedMsg {
|
||||
chat_id,
|
||||
state,
|
||||
hidden,
|
||||
sort_timestamp,
|
||||
msg_ids: vec![],
|
||||
needs_delete_job: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut param = part.param.clone();
|
||||
@@ -2373,10 +2396,7 @@ async fn handle_edit_delete(
|
||||
warn!(context, "Edit message: Database entry does not exist.");
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Edit message: rfc724_mid {rfc724_mid:?} not found."
|
||||
);
|
||||
bail!("Edit message: rfc724_mid {rfc724_mid:?} not found (SKIP_DEVICE_MSG)");
|
||||
}
|
||||
} else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
|
||||
&& let Some(part) = mime_parser.parts.first()
|
||||
|
||||
@@ -14,7 +14,9 @@ use crate::contact;
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::imex::{ImexMode, imex};
|
||||
use crate::key;
|
||||
use crate::message::markseen_msgs;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::smtp;
|
||||
use crate::test_utils::{
|
||||
TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
|
||||
};
|
||||
@@ -2655,6 +2657,32 @@ async fn test_read_receipts_dont_unmark_bots() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_self_mdn_before_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob2 = &tcm.bob().await;
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
|
||||
let sent = alice.send_text(alice_chat.id, "hi").await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
msg.chat_id.accept(bob).await?;
|
||||
markseen_msgs(bob, vec![msg.id]).await?;
|
||||
smtp::queue_mdn(bob).await?;
|
||||
let sent_mdn = bob.pop_sent_msg().await;
|
||||
|
||||
let Err(err) = receive_imf(bob2, sent_mdn.payload().as_bytes(), false).await else {
|
||||
unreachable!();
|
||||
};
|
||||
assert!(format!("{err:#}").contains("(SKIP_DEVICE_MSG)"));
|
||||
let msg = bob2.recv_msg(&sent).await;
|
||||
assert_eq!(msg.get_state(), MessageState::InFresh);
|
||||
bob2.recv_msg_trash(&sent_mdn).await;
|
||||
assert_eq!(msg.id.get_state(bob2).await?, MessageState::InSeen);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_gmx_forwarded_msg() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
103
src/smtp.rs
103
src/smtp.rs
@@ -13,7 +13,7 @@ use crate::config::Config;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
@@ -590,44 +590,77 @@ async fn send_mdn_rfc724_mid(
|
||||
if context.get_config_bool(Config::BccSelf).await? {
|
||||
add_self_recipients(context, &mut recipients, encrypted).await?;
|
||||
}
|
||||
let recipients: Vec<_> = recipients
|
||||
.into_iter()
|
||||
.filter_map(|addr| {
|
||||
async_smtp::EmailAddress::new(addr.clone())
|
||||
.with_context(|| format!("Invalid recipient: {addr}"))
|
||||
.log_err(context)
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
use crate::log::LogExt;
|
||||
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
SendResult::Success => {
|
||||
if !recipients.is_empty() {
|
||||
info!(context, "Successfully sent MDN for {rfc724_mid}.");
|
||||
let recipients: Vec<_> = recipients
|
||||
.into_iter()
|
||||
.filter_map(|addr| {
|
||||
async_smtp::EmailAddress::new(addr.clone())
|
||||
.with_context(|| format!("Invalid recipient: {addr}"))
|
||||
.log_err(context)
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
SendResult::Success => {
|
||||
if !recipients.is_empty() {
|
||||
info!(context, "Successfully sent MDN for {rfc724_mid}.");
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt =
|
||||
transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
|
||||
stmt.execute((rfc724_mid,))?;
|
||||
for additional_rfc724_mid in additional_rfc724_mids {
|
||||
stmt.execute((additional_rfc724_mid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(true)
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt =
|
||||
transaction.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
|
||||
stmt.execute((rfc724_mid,))?;
|
||||
for additional_rfc724_mid in additional_rfc724_mids {
|
||||
stmt.execute((additional_rfc724_mid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(true)
|
||||
SendResult::Retry => {
|
||||
info!(
|
||||
context,
|
||||
"Temporary SMTP failure while sending an MDN for {rfc724_mid}."
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
SendResult::Failure(err) => Err(err),
|
||||
}
|
||||
SendResult::Retry => {
|
||||
info!(
|
||||
context,
|
||||
"Temporary SMTP failure while sending an MDN for {rfc724_mid}."
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
SendResult::Failure(err) => Err(err),
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
let _ = smtp;
|
||||
context
|
||||
.sql
|
||||
.transaction(|t| {
|
||||
t.execute(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
(rfc724_mid, recipients.join(" "), body, u32::MAX),
|
||||
)?;
|
||||
let mut stmt = t.prepare("DELETE FROM smtp_mdns WHERE rfc724_mid = ?")?;
|
||||
stmt.execute((rfc724_mid,))?;
|
||||
for additional_rfc724_mid in additional_rfc724_mids {
|
||||
stmt.execute((additional_rfc724_mid,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn queue_mdn(context: &Context) -> Result<()> {
|
||||
let queued = send_mdn(context, &mut Smtp::new()).await?;
|
||||
assert!(queued);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to send a single MDN. Returns true if more MDNs should be sent.
|
||||
|
||||
@@ -1431,6 +1431,12 @@ pub fn fiona_keypair() -> SignedSecretKey {
|
||||
#[derive(Debug)]
|
||||
pub struct EventTracker(EventEmitter);
|
||||
|
||||
/// See [`super::EventTracker::get_matching_ex`].
|
||||
pub struct ExpectedEvents<E: Fn(&EventType) -> bool, U: Fn(&EventType) -> bool> {
|
||||
pub expected: E,
|
||||
pub unexpected: U,
|
||||
}
|
||||
|
||||
impl Deref for EventTracker {
|
||||
type Target = EventEmitter;
|
||||
|
||||
@@ -1467,21 +1473,39 @@ impl EventTracker {
|
||||
.expect("timeout waiting for event match")
|
||||
}
|
||||
|
||||
/// Consumes emitted events returning the first matching one if any.
|
||||
/// Consumes all emitted events returning the first matching one if any.
|
||||
pub async fn get_matching_opt<F: Fn(&EventType) -> bool>(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
event_matcher: F,
|
||||
) -> Option<EventType> {
|
||||
self.get_matching_ex(
|
||||
ctx,
|
||||
ExpectedEvents {
|
||||
expected: event_matcher,
|
||||
unexpected: |_| false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Consumes all emitted events returning the first matching one if any. Panics on unexpected
|
||||
/// events.
|
||||
pub async fn get_matching_ex<E: Fn(&EventType) -> bool, U: Fn(&EventType) -> bool>(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
args: ExpectedEvents<E, U>,
|
||||
) -> Option<EventType> {
|
||||
ctx.emit_event(EventType::Test);
|
||||
let mut found_event = None;
|
||||
loop {
|
||||
let event = self.recv().await.unwrap();
|
||||
assert!(!(args.unexpected)(&event.typ));
|
||||
if let EventType::Test = event.typ {
|
||||
return found_event;
|
||||
}
|
||||
if event_matcher(&event.typ) {
|
||||
found_event = Some(event.typ);
|
||||
if (args.expected)(&event.typ) {
|
||||
found_event.get_or_insert(event.typ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user