mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 01:16:31 +03:00
feat: send pre-message on messages with large attachments (#7410)
part of #7367 progress / what's to do: - [x] send pre-message - [x] The message's state must be set to MessageState::OutDelivered only after both messages are sent. If a read receipt is received, the message can be OutMdnRcvd or OutPending; let's just do whatever is easiest for now. Take care not to revert from OutMdnReceived to OutDelivered if we first receive a read receipt and then deliver the full message. - this is already the case: - `OutDelivered` is set when a message is sent out and has no remaining send jobs in the smtp table for this message id - so already works since full message and pre message have same msgId in that table. - `OutMdnRcvd` is a "virtual" state (https://github.com/chatmail/core/issues/7367#issuecomment-3500891040), so going back to `OutDelivered` can't happen anymore - [x] delimit `ChatFullMessageId` with `<` and `>` like the other message ids - [x] add tests - [x] test that pre message is sent for attachment larger than X - test that correct headers are present on both messages - also test that Autocrypt-gossip and selfavatar should never go into full-messages - [x] test that no pre message is sent for attachment smaller than X - no "is full message" header should be present - [x] test that pre message is not send for large webxdc update or large text - [x] fix test `receive_imf::receive_imf_tests::test_dont_reverify_by_self_on_outgoing_msg` --------- Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com> Co-authored-by: Hocuri <hocuri@gmx.de>
This commit is contained in:
73
src/chat.rs
73
src/chat.rs
@@ -27,7 +27,9 @@ use crate::constants::{
|
|||||||
use crate::contact::{self, Contact, ContactId, Origin};
|
use crate::contact::{self, Contact, ContactId, Origin};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::debug_logging::maybe_set_logging_xdc;
|
use crate::debug_logging::maybe_set_logging_xdc;
|
||||||
use crate::download::DownloadState;
|
use crate::download::{
|
||||||
|
DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PRE_MSG_SIZE_WARNING_THRESHOLD,
|
||||||
|
};
|
||||||
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||||
use crate::events::EventType;
|
use crate::events::EventType;
|
||||||
use crate::key::self_fingerprint;
|
use crate::key::self_fingerprint;
|
||||||
@@ -35,7 +37,7 @@ use crate::location;
|
|||||||
use crate::log::{LogExt, warn};
|
use crate::log::{LogExt, warn};
|
||||||
use crate::logged_debug_assert;
|
use crate::logged_debug_assert;
|
||||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||||
use crate::mimefactory::MimeFactory;
|
use crate::mimefactory::{MimeFactory, RenderedEmail};
|
||||||
use crate::mimeparser::SystemMessage;
|
use crate::mimeparser::SystemMessage;
|
||||||
use crate::param::{Param, Params};
|
use crate::param::{Param, Params};
|
||||||
use crate::receive_imf::ReceivedMsg;
|
use crate::receive_imf::ReceivedMsg;
|
||||||
@@ -2728,6 +2730,52 @@ async fn prepare_send_msg(
|
|||||||
Ok(row_ids)
|
Ok(row_ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Renders the message or Full-Message and Pre-Message.
|
||||||
|
///
|
||||||
|
/// Pre-Message is a small message with metadata which announces a larger Full-Message.
|
||||||
|
/// Full messages are not downloaded in the background.
|
||||||
|
///
|
||||||
|
/// If pre-message is not nessesary this returns a normal message instead.
|
||||||
|
async fn render_mime_message_and_pre_message(
|
||||||
|
context: &Context,
|
||||||
|
msg: &mut Message,
|
||||||
|
mimefactory: MimeFactory,
|
||||||
|
) -> Result<(RenderedEmail, Option<RenderedEmail>)> {
|
||||||
|
let needs_pre_message = msg.viewtype.has_file()
|
||||||
|
&& mimefactory.will_be_encrypted() // unencrypted is likely email, we don't want to spam by sending multiple messages
|
||||||
|
&& msg
|
||||||
|
.get_filebytes(context)
|
||||||
|
.await?
|
||||||
|
.context("filebytes not available, even though message has attachment")?
|
||||||
|
> PRE_MSG_ATTACHMENT_SIZE_THRESHOLD;
|
||||||
|
|
||||||
|
if needs_pre_message {
|
||||||
|
let mut mimefactory_full_msg = mimefactory.clone();
|
||||||
|
mimefactory_full_msg.set_as_full_message();
|
||||||
|
let rendered_msg = mimefactory_full_msg.render(context).await?;
|
||||||
|
|
||||||
|
let mut mimefactory_pre_msg = mimefactory;
|
||||||
|
mimefactory_pre_msg.set_as_pre_message_for(&rendered_msg);
|
||||||
|
let rendered_pre_msg = mimefactory_pre_msg
|
||||||
|
.render(context)
|
||||||
|
.await
|
||||||
|
.context("pre-message failed to render")?;
|
||||||
|
|
||||||
|
if rendered_pre_msg.message.len() > PRE_MSG_SIZE_WARNING_THRESHOLD {
|
||||||
|
warn!(
|
||||||
|
context,
|
||||||
|
"Pre-message for message (MsgId={}) is larger than expected: {}.",
|
||||||
|
msg.id,
|
||||||
|
rendered_pre_msg.message.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((rendered_msg, Some(rendered_pre_msg)))
|
||||||
|
} else {
|
||||||
|
Ok((mimefactory.render(context).await?, None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Constructs jobs for sending a message and inserts them into the appropriate table.
|
/// Constructs jobs for sending a message and inserts them into the appropriate table.
|
||||||
///
|
///
|
||||||
/// Updates the message `GuaranteeE2ee` parameter and persists it
|
/// Updates the message `GuaranteeE2ee` parameter and persists it
|
||||||
@@ -2799,7 +2847,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let rendered_msg = match mimefactory.render(context).await {
|
let (rendered_msg, rendered_pre_msg) =
|
||||||
|
match render_mime_message_and_pre_message(context, msg, mimefactory).await {
|
||||||
Ok(res) => Ok(res),
|
Ok(res) => Ok(res),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
message::set_msg_failed(context, msg, &err.to_string()).await?;
|
message::set_msg_failed(context, msg, &err.to_string()).await?;
|
||||||
@@ -2864,12 +2913,26 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
|||||||
} else {
|
} else {
|
||||||
for recipients_chunk in recipients.chunks(chunk_size) {
|
for recipients_chunk in recipients.chunks(chunk_size) {
|
||||||
let recipients_chunk = recipients_chunk.join(" ");
|
let recipients_chunk = recipients_chunk.join(" ");
|
||||||
|
// send pre-message before actual message
|
||||||
|
if let Some(pre_msg) = &rendered_pre_msg {
|
||||||
let row_id = t.execute(
|
let row_id = t.execute(
|
||||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
|
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
(
|
||||||
|
&pre_msg.rfc724_mid,
|
||||||
|
&recipients_chunk,
|
||||||
|
&pre_msg.message,
|
||||||
|
msg.id,
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
row_ids.push(row_id.try_into()?);
|
||||||
|
}
|
||||||
|
let row_id = t.execute(
|
||||||
|
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||||
VALUES (?1, ?2, ?3, ?4)",
|
VALUES (?1, ?2, ?3, ?4)",
|
||||||
(
|
(
|
||||||
&rendered_msg.rfc724_mid,
|
&rendered_msg.rfc724_mid,
|
||||||
recipients_chunk,
|
&recipients_chunk,
|
||||||
&rendered_msg.message,
|
&rendered_msg.message,
|
||||||
msg.id,
|
msg.id,
|
||||||
),
|
),
|
||||||
|
|||||||
348
src/download.rs
348
src/download.rs
@@ -17,6 +17,22 @@ use crate::{EventType, chatlist_events};
|
|||||||
/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case.
|
/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case.
|
||||||
pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
|
pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
|
||||||
|
|
||||||
|
/// From this point onward outgoing messages are considered large
|
||||||
|
/// and get a pre-message, which announces the full message.
|
||||||
|
// this is only about sending so we can modify it any time.
|
||||||
|
// current value is a bit less than the minimum auto download setting from the UIs (which is 160 KiB)
|
||||||
|
pub(crate) const PRE_MSG_ATTACHMENT_SIZE_THRESHOLD: u64 = 140_000;
|
||||||
|
|
||||||
|
/// Max message size to be fetched in the background.
|
||||||
|
/// This limit defines what messages are fully fetched in the background.
|
||||||
|
/// This is for all messages that don't have the Chat-Is-Full-Message header.
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) const MAX_FETCH_MSG_SIZE: usize = 1_000_000;
|
||||||
|
|
||||||
|
/// Max size for pre messages. A warning is emitted when this is exceeded.
|
||||||
|
/// Should be well below `MAX_FETCH_MSG_SIZE`
|
||||||
|
pub(crate) const PRE_MSG_SIZE_WARNING_THRESHOLD: usize = 150_000;
|
||||||
|
|
||||||
/// Download state of the message.
|
/// Download state of the message.
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
@@ -192,12 +208,17 @@ impl Session {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use mailparse::MailHeaderMap;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::chat::send_msg;
|
use crate::chat::{self, create_group, send_msg};
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||||
|
use crate::message::Viewtype;
|
||||||
use crate::receive_imf::receive_imf_from_inbox;
|
use crate::receive_imf::receive_imf_from_inbox;
|
||||||
use crate::test_utils::TestContext;
|
use crate::test_utils::{self, TestContext, TestContextManager};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_downloadstate_values() {
|
fn test_downloadstate_values() {
|
||||||
@@ -294,4 +315,327 @@ mod tests {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
/// Tests that pre message is sent for attachment larger than `PRE_MSG_ATTACHMENT_SIZE_THRESHOLD`
|
||||||
|
/// Also test that pre message is sent first, before the full message
|
||||||
|
/// And that Autocrypt-gossip and selfavatar never go into full-messages
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_sending_pre_message() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
let fiona = &tcm.fiona().await;
|
||||||
|
let group_id = alice
|
||||||
|
.create_group_with_members("test group", &[bob, fiona])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut msg = Message::new(Viewtype::File);
|
||||||
|
msg.set_file_from_bytes(alice, "test.bin", &[0u8; 300_000], None)?;
|
||||||
|
msg.set_text("test".to_owned());
|
||||||
|
|
||||||
|
// assert that test attachment is bigger than limit
|
||||||
|
assert!(msg.get_filebytes(alice).await?.unwrap() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD);
|
||||||
|
|
||||||
|
let msg_id = chat::send_msg(alice, group_id, &mut msg).await?;
|
||||||
|
let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await;
|
||||||
|
|
||||||
|
// pre-message and full message should be present
|
||||||
|
// and test that correct headers are present on both messages
|
||||||
|
assert_eq!(smtp_rows.len(), 2);
|
||||||
|
let pre_message = smtp_rows.first().expect("first element exists");
|
||||||
|
let pre_message_parsed = mailparse::parse_mail(pre_message.payload.as_bytes())?;
|
||||||
|
let full_message = smtp_rows.get(1).expect("second element exists");
|
||||||
|
let full_message_parsed = mailparse::parse_mail(full_message.payload.as_bytes())?;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
pre_message_parsed
|
||||||
|
.headers
|
||||||
|
.get_first_header(HeaderDef::ChatIsFullMessage.get_headername())
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
full_message_parsed
|
||||||
|
.headers
|
||||||
|
.get_first_header(HeaderDef::ChatIsFullMessage.get_headername())
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
full_message_parsed
|
||||||
|
.headers
|
||||||
|
.get_header_value(HeaderDef::MessageId),
|
||||||
|
Some(format!("<{}>", msg.rfc724_mid)),
|
||||||
|
"full message should have the rfc message id of the database message"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_ne!(
|
||||||
|
pre_message_parsed
|
||||||
|
.headers
|
||||||
|
.get_header_value(HeaderDef::MessageId),
|
||||||
|
full_message_parsed
|
||||||
|
.headers
|
||||||
|
.get_header_value(HeaderDef::MessageId),
|
||||||
|
"message ids of pre message and full message should be different"
|
||||||
|
);
|
||||||
|
|
||||||
|
let decrypted_full_message = bob.parse_msg(full_message).await;
|
||||||
|
assert_eq!(decrypted_full_message.decrypting_failed, false);
|
||||||
|
assert_eq!(
|
||||||
|
decrypted_full_message.header_exists(HeaderDef::ChatFullMessageId),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
let decrypted_pre_message = bob.parse_msg(pre_message).await;
|
||||||
|
assert_eq!(
|
||||||
|
decrypted_pre_message
|
||||||
|
.get_header(HeaderDef::ChatFullMessageId)
|
||||||
|
.map(String::from),
|
||||||
|
full_message_parsed
|
||||||
|
.headers
|
||||||
|
.get_header_value(HeaderDef::MessageId)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pre_message_parsed
|
||||||
|
.headers
|
||||||
|
.get_header_value(HeaderDef::ChatFullMessageId)
|
||||||
|
.is_none(),
|
||||||
|
"no Chat-Full-Message-ID header in unprotected headers of Pre-Message"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that pre message has autocrypt gossip headers and self avatar
|
||||||
|
/// and full message doesn't have these headers
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_selfavatar_and_autocrypt_gossip_goto_pre_message() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
let fiona = &tcm.fiona().await;
|
||||||
|
let group_id = alice
|
||||||
|
.create_group_with_members("test group", &[bob, fiona])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut msg = Message::new(Viewtype::File);
|
||||||
|
msg.set_file_from_bytes(alice, "test.bin", &[0u8; 300_000], None)?;
|
||||||
|
msg.set_text("test".to_owned());
|
||||||
|
|
||||||
|
// assert that test attachment is bigger than limit
|
||||||
|
assert!(msg.get_filebytes(alice).await?.unwrap() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD);
|
||||||
|
|
||||||
|
// simulate conditions for sending self avatar
|
||||||
|
let avatar_src = alice.get_blobdir().join("avatar.png");
|
||||||
|
fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES).await?;
|
||||||
|
alice
|
||||||
|
.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let msg_id = chat::send_msg(alice, group_id, &mut msg).await?;
|
||||||
|
let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await;
|
||||||
|
|
||||||
|
assert_eq!(smtp_rows.len(), 2);
|
||||||
|
let pre_message = smtp_rows.first().expect("first element exists");
|
||||||
|
let full_message = smtp_rows.get(1).expect("second element exists");
|
||||||
|
let full_message_parsed = mailparse::parse_mail(full_message.payload.as_bytes())?;
|
||||||
|
|
||||||
|
let decrypted_pre_message = bob.parse_msg(pre_message).await;
|
||||||
|
assert!(
|
||||||
|
decrypted_pre_message
|
||||||
|
.get_header(HeaderDef::ChatFullMessageId)
|
||||||
|
.is_some(),
|
||||||
|
"tested message is not a pre-message, sending order may be broken"
|
||||||
|
);
|
||||||
|
assert_ne!(decrypted_pre_message.gossiped_keys.len(), 0);
|
||||||
|
assert_ne!(decrypted_pre_message.user_avatar, None);
|
||||||
|
|
||||||
|
let decrypted_full_message = bob.parse_msg(full_message).await;
|
||||||
|
assert!(
|
||||||
|
full_message_parsed
|
||||||
|
.headers
|
||||||
|
.get_first_header(HeaderDef::ChatIsFullMessage.get_headername())
|
||||||
|
.is_some(),
|
||||||
|
"tested message is not a full-message, sending order may be broken"
|
||||||
|
);
|
||||||
|
assert_eq!(decrypted_full_message.gossiped_keys.len(), 0);
|
||||||
|
assert_eq!(decrypted_full_message.user_avatar, None);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_unecrypted_gets_no_pre_message() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
|
||||||
|
let chat = alice
|
||||||
|
.create_chat_with_contact("example", "email@example.org")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut msg = Message::new(Viewtype::File);
|
||||||
|
msg.set_file_from_bytes(alice, "test.bin", &[0u8; 300_000], None)?;
|
||||||
|
msg.set_text("test".to_owned());
|
||||||
|
|
||||||
|
let msg_id = chat::send_msg(alice, chat.id, &mut msg).await?;
|
||||||
|
let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await;
|
||||||
|
|
||||||
|
assert_eq!(smtp_rows.len(), 1);
|
||||||
|
let message_bytes = smtp_rows
|
||||||
|
.first()
|
||||||
|
.expect("first element exists")
|
||||||
|
.payload
|
||||||
|
.as_bytes();
|
||||||
|
let message = mailparse::parse_mail(message_bytes)?;
|
||||||
|
assert!(
|
||||||
|
message
|
||||||
|
.headers
|
||||||
|
.get_first_header(HeaderDef::ChatIsFullMessage.get_headername())
|
||||||
|
.is_none(),
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that no pre message is sent for normal message
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_not_sending_pre_message_no_attachment() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
let chat = alice.create_chat(bob).await;
|
||||||
|
|
||||||
|
// send normal text message
|
||||||
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
|
msg.set_text("test".to_owned());
|
||||||
|
let msg_id = chat::send_msg(alice, chat.id, &mut msg).await.unwrap();
|
||||||
|
let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await;
|
||||||
|
|
||||||
|
assert_eq!(smtp_rows.len(), 1, "only one message should be sent");
|
||||||
|
|
||||||
|
let msg = smtp_rows.first().expect("first element exists");
|
||||||
|
let mail = mailparse::parse_mail(msg.payload.as_bytes())?;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
mail.headers
|
||||||
|
.get_first_header(HeaderDef::ChatIsFullMessage.get_headername())
|
||||||
|
.is_none(),
|
||||||
|
"no 'Chat-Is-Full-Message'-header should be present"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
mail.headers
|
||||||
|
.get_first_header(HeaderDef::ChatFullMessageId.get_headername())
|
||||||
|
.is_none(),
|
||||||
|
"no 'Chat-Full-Message-ID'-header should be present in clear text headers"
|
||||||
|
);
|
||||||
|
let decrypted_message = bob.parse_msg(msg).await;
|
||||||
|
assert!(
|
||||||
|
!decrypted_message.header_exists(HeaderDef::ChatFullMessageId),
|
||||||
|
"no 'Chat-Full-Message-ID'-header should be present"
|
||||||
|
);
|
||||||
|
|
||||||
|
// test that pre message is not send for large large text
|
||||||
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
|
let long_text = String::from_utf8(vec![b'a'; 300_000])?;
|
||||||
|
assert!(long_text.len() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap());
|
||||||
|
msg.set_text(long_text);
|
||||||
|
let msg_id = chat::send_msg(alice, chat.id, &mut msg).await.unwrap();
|
||||||
|
let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await;
|
||||||
|
|
||||||
|
assert_eq!(smtp_rows.len(), 1, "only one message should be sent");
|
||||||
|
|
||||||
|
let msg = smtp_rows.first().expect("first element exists");
|
||||||
|
let mail = mailparse::parse_mail(msg.payload.as_bytes())?;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
mail.headers
|
||||||
|
.get_first_header(HeaderDef::ChatIsFullMessage.get_headername())
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
mail.headers
|
||||||
|
.get_first_header(HeaderDef::ChatFullMessageId.get_headername())
|
||||||
|
.is_none(),
|
||||||
|
"no 'Chat-Full-Message-ID'-header should be present in clear text headers"
|
||||||
|
);
|
||||||
|
let decrypted_message = bob.parse_msg(msg).await;
|
||||||
|
assert!(
|
||||||
|
!decrypted_message.header_exists(HeaderDef::ChatFullMessageId),
|
||||||
|
"no 'Chat-Full-Message-ID'-header should be present"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that no pre message is sent for attachment smaller than `PRE_MSG_ATTACHMENT_SIZE_THRESHOLD`
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_not_sending_pre_message_for_small_attachment() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
let chat = alice.create_chat(bob).await;
|
||||||
|
|
||||||
|
let mut msg = Message::new(Viewtype::File);
|
||||||
|
msg.set_file_from_bytes(alice, "test.bin", &[0u8; 100_000], None)?;
|
||||||
|
msg.set_text("test".to_owned());
|
||||||
|
|
||||||
|
// assert that test attachment is smaller than limit
|
||||||
|
assert!(msg.get_filebytes(alice).await?.unwrap() < PRE_MSG_ATTACHMENT_SIZE_THRESHOLD);
|
||||||
|
|
||||||
|
let msg_id = chat::send_msg(alice, chat.id, &mut msg).await.unwrap();
|
||||||
|
let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await;
|
||||||
|
|
||||||
|
// only one message and no "is full message" header should be present
|
||||||
|
assert_eq!(smtp_rows.len(), 1);
|
||||||
|
|
||||||
|
let msg = smtp_rows.first().expect("first element exists");
|
||||||
|
let mail = mailparse::parse_mail(msg.payload.as_bytes())?;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
mail.headers
|
||||||
|
.get_first_header(HeaderDef::ChatIsFullMessage.get_headername())
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
mail.headers
|
||||||
|
.get_first_header(HeaderDef::ChatFullMessageId.get_headername())
|
||||||
|
.is_none(),
|
||||||
|
"no 'Chat-Full-Message-ID'-header should be present in clear text headers"
|
||||||
|
);
|
||||||
|
let decrypted_message = bob.parse_msg(msg).await;
|
||||||
|
assert!(
|
||||||
|
!decrypted_message.header_exists(HeaderDef::ChatFullMessageId),
|
||||||
|
"no 'Chat-Full-Message-ID'-header should be present"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests that pre message is not send for large webxdc updates
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_render_webxdc_status_update_object_range() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
let chat_id = create_group(&t, "a chat").await?;
|
||||||
|
|
||||||
|
let instance = {
|
||||||
|
let mut instance = Message::new(Viewtype::File);
|
||||||
|
instance.set_file_from_bytes(
|
||||||
|
&t,
|
||||||
|
"minimal.xdc",
|
||||||
|
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
let instance_msg_id = send_msg(&t, chat_id, &mut instance).await?;
|
||||||
|
assert_eq!(instance.viewtype, Viewtype::Webxdc);
|
||||||
|
Message::load_from_db(&t, instance_msg_id).await
|
||||||
|
}
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
t.pop_sent_msg().await;
|
||||||
|
assert_eq!(t.sql.count("SELECT COUNT(*) FROM smtp", ()).await?, 0);
|
||||||
|
|
||||||
|
let long_text = String::from_utf8(vec![b'a'; 300_000])?;
|
||||||
|
assert!(long_text.len() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap());
|
||||||
|
t.send_webxdc_status_update(instance.id, &format!("{{\"payload\": \"{long_text}\"}}"))
|
||||||
|
.await?;
|
||||||
|
t.flush_status_updates().await?;
|
||||||
|
|
||||||
|
assert_eq!(t.sql.count("SELECT COUNT(*) FROM smtp", ()).await?, 1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,17 @@ pub enum HeaderDef {
|
|||||||
/// used to encrypt and decrypt messages.
|
/// used to encrypt and decrypt messages.
|
||||||
/// This secret is sent to a new member in the member-addition message.
|
/// This secret is sent to a new member in the member-addition message.
|
||||||
ChatBroadcastSecret,
|
ChatBroadcastSecret,
|
||||||
|
/// A message with a large attachment is split into two MIME messages:
|
||||||
|
/// A pre-message, which contains everything but the attachment,
|
||||||
|
/// and a full-message.
|
||||||
|
/// The pre-message gets a `Chat-Full-Message-Id` header
|
||||||
|
/// referencing the full-message's rfc724_mid.
|
||||||
|
ChatFullMessageId,
|
||||||
|
|
||||||
|
/// This message is preceded by a pre-message
|
||||||
|
/// and thus this message can be skipped while fetching messages.
|
||||||
|
/// This is a cleartext / unproteced header.
|
||||||
|
ChatIsFullMessage,
|
||||||
|
|
||||||
/// [Autocrypt](https://autocrypt.org/) header.
|
/// [Autocrypt](https://autocrypt.org/) header.
|
||||||
Autocrypt,
|
Autocrypt,
|
||||||
@@ -147,6 +158,9 @@ pub enum HeaderDef {
|
|||||||
|
|
||||||
impl HeaderDef {
|
impl HeaderDef {
|
||||||
/// Returns the corresponding header string.
|
/// Returns the corresponding header string.
|
||||||
|
///
|
||||||
|
/// Format is lower-kebab-case for easy comparisons.
|
||||||
|
/// This method is used in message receiving and testing.
|
||||||
pub fn get_headername(&self) -> &'static str {
|
pub fn get_headername(&self) -> &'static str {
|
||||||
self.into()
|
self.into()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ pub enum Loaded {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum PreMessageMode {
|
||||||
|
/// adds the Chat-Is-Full-Message header in unprotected part
|
||||||
|
FullMessage,
|
||||||
|
/// adds the Chat-Full-Message-ID header to protected part
|
||||||
|
/// also adds metadata and explicitly excludes attachment
|
||||||
|
PreMessage { full_msg_rfc724_mid: String },
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper to construct mime messages.
|
/// Helper to construct mime messages.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MimeFactory {
|
pub struct MimeFactory {
|
||||||
@@ -146,6 +155,9 @@ pub struct MimeFactory {
|
|||||||
|
|
||||||
/// This field is used to sustain the topic id of webxdcs needed for peer channels.
|
/// This field is used to sustain the topic id of webxdcs needed for peer channels.
|
||||||
webxdc_topic: Option<TopicId>,
|
webxdc_topic: Option<TopicId>,
|
||||||
|
|
||||||
|
/// This field is used when this is either a pre-message or a full-message.
|
||||||
|
pre_message_mode: Option<PreMessageMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of rendering a message, ready to be submitted to a send job.
|
/// Result of rendering a message, ready to be submitted to a send job.
|
||||||
@@ -500,6 +512,7 @@ impl MimeFactory {
|
|||||||
sync_ids_to_delete: None,
|
sync_ids_to_delete: None,
|
||||||
attach_selfavatar,
|
attach_selfavatar,
|
||||||
webxdc_topic,
|
webxdc_topic,
|
||||||
|
pre_message_mode: None,
|
||||||
};
|
};
|
||||||
Ok(factory)
|
Ok(factory)
|
||||||
}
|
}
|
||||||
@@ -548,6 +561,7 @@ impl MimeFactory {
|
|||||||
sync_ids_to_delete: None,
|
sync_ids_to_delete: None,
|
||||||
attach_selfavatar: false,
|
attach_selfavatar: false,
|
||||||
webxdc_topic: None,
|
webxdc_topic: None,
|
||||||
|
pre_message_mode: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
@@ -779,7 +793,10 @@ impl MimeFactory {
|
|||||||
headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into()));
|
headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into()));
|
||||||
|
|
||||||
let rfc724_mid = match &self.loaded {
|
let rfc724_mid = match &self.loaded {
|
||||||
Loaded::Message { msg, .. } => msg.rfc724_mid.clone(),
|
Loaded::Message { msg, .. } => match &self.pre_message_mode {
|
||||||
|
Some(PreMessageMode::PreMessage { .. }) => create_outgoing_rfc724_mid(),
|
||||||
|
_ => msg.rfc724_mid.clone(),
|
||||||
|
},
|
||||||
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),
|
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),
|
||||||
};
|
};
|
||||||
headers.push((
|
headers.push((
|
||||||
@@ -893,7 +910,7 @@ impl MimeFactory {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_encrypted = self.encryption_pubkeys.is_some();
|
let is_encrypted = self.will_be_encrypted();
|
||||||
|
|
||||||
// Add ephemeral timer for non-MDN messages.
|
// Add ephemeral timer for non-MDN messages.
|
||||||
// For MDNs it does not matter because they are not visible
|
// For MDNs it does not matter because they are not visible
|
||||||
@@ -978,6 +995,22 @@ impl MimeFactory {
|
|||||||
"MIME-Version",
|
"MIME-Version",
|
||||||
mail_builder::headers::raw::Raw::new("1.0").into(),
|
mail_builder::headers::raw::Raw::new("1.0").into(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if self.pre_message_mode == Some(PreMessageMode::FullMessage) {
|
||||||
|
unprotected_headers.push((
|
||||||
|
"Chat-Is-Full-Message",
|
||||||
|
mail_builder::headers::raw::Raw::new("1").into(),
|
||||||
|
));
|
||||||
|
} else if let Some(PreMessageMode::PreMessage {
|
||||||
|
full_msg_rfc724_mid,
|
||||||
|
}) = self.pre_message_mode.clone()
|
||||||
|
{
|
||||||
|
protected_headers.push((
|
||||||
|
"Chat-Full-Message-ID",
|
||||||
|
mail_builder::headers::message_id::MessageId::new(full_msg_rfc724_mid).into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
for header @ (original_header_name, _header_value) in &headers {
|
for header @ (original_header_name, _header_value) in &headers {
|
||||||
let header_name = original_header_name.to_lowercase();
|
let header_name = original_header_name.to_lowercase();
|
||||||
if header_name == "message-id" {
|
if header_name == "message-id" {
|
||||||
@@ -1119,6 +1152,10 @@ impl MimeFactory {
|
|||||||
for (addr, key) in &encryption_pubkeys {
|
for (addr, key) in &encryption_pubkeys {
|
||||||
let fingerprint = key.dc_fingerprint().hex();
|
let fingerprint = key.dc_fingerprint().hex();
|
||||||
let cmd = msg.param.get_cmd();
|
let cmd = msg.param.get_cmd();
|
||||||
|
if self.pre_message_mode == Some(PreMessageMode::FullMessage) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
|
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
|
||||||
|| cmd == SystemMessage::SecurejoinMessage
|
|| cmd == SystemMessage::SecurejoinMessage
|
||||||
|| multiple_recipients && {
|
|| multiple_recipients && {
|
||||||
@@ -1875,9 +1912,13 @@ impl MimeFactory {
|
|||||||
|
|
||||||
// add attachment part
|
// add attachment part
|
||||||
if msg.viewtype.has_file() {
|
if msg.viewtype.has_file() {
|
||||||
|
if let Some(PreMessageMode::PreMessage { .. }) = self.pre_message_mode {
|
||||||
|
// TODO: generate thumbnail and attach it instead (if it makes sense)
|
||||||
|
} else {
|
||||||
let file_part = build_body_file(context, &msg).await?;
|
let file_part = build_body_file(context, &msg).await?;
|
||||||
parts.push(file_part);
|
parts.push(file_part);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(msg_kml_part) = self.get_message_kml_part() {
|
if let Some(msg_kml_part) = self.get_message_kml_part() {
|
||||||
parts.push(msg_kml_part);
|
parts.push(msg_kml_part);
|
||||||
@@ -1921,6 +1962,8 @@ impl MimeFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.attach_selfavatar =
|
||||||
|
self.attach_selfavatar && self.pre_message_mode != Some(PreMessageMode::FullMessage);
|
||||||
if self.attach_selfavatar {
|
if self.attach_selfavatar {
|
||||||
match context.get_config(Config::Selfavatar).await? {
|
match context.get_config(Config::Selfavatar).await? {
|
||||||
Some(path) => match build_avatar_file(context, &path).await {
|
Some(path) => match build_avatar_file(context, &path).await {
|
||||||
@@ -1990,6 +2033,20 @@ impl MimeFactory {
|
|||||||
|
|
||||||
Ok(message)
|
Ok(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn will_be_encrypted(&self) -> bool {
|
||||||
|
self.encryption_pubkeys.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_as_full_message(&mut self) {
|
||||||
|
self.pre_message_mode = Some(PreMessageMode::FullMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_as_pre_message_for(&mut self, full_message: &RenderedEmail) {
|
||||||
|
self.pre_message_mode = Some(PreMessageMode::PreMessage {
|
||||||
|
full_msg_rfc724_mid: full_message.rfc724_mid.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hidden_recipients() -> Address<'static> {
|
fn hidden_recipients() -> Address<'static> {
|
||||||
|
|||||||
@@ -711,6 +711,32 @@ impl TestContext {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_smtp_rows_for_msg<'a>(&'a self, msg_id: MsgId) -> Vec<SentMessage<'a>> {
|
||||||
|
self.ctx
|
||||||
|
.sql
|
||||||
|
.query_map_vec(
|
||||||
|
"SELECT id, msg_id, mime, recipients FROM smtp WHERE msg_id=?",
|
||||||
|
(msg_id,),
|
||||||
|
|row| {
|
||||||
|
let _id: MsgId = row.get(0)?;
|
||||||
|
let msg_id: MsgId = row.get(1)?;
|
||||||
|
let mime: String = row.get(2)?;
|
||||||
|
let recipients: String = row.get(3)?;
|
||||||
|
Ok((msg_id, mime, recipients))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(msg_id, mime, recipients)| SentMessage {
|
||||||
|
payload: mime,
|
||||||
|
sender_msg_id: msg_id,
|
||||||
|
sender_context: &self.ctx,
|
||||||
|
recipients,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieves a sent sync message from the db.
|
/// Retrieves a sent sync message from the db.
|
||||||
///
|
///
|
||||||
/// This retrieves and removes a sync message which has been scheduled to send from the jobs
|
/// This retrieves and removes a sync message which has been scheduled to send from the jobs
|
||||||
|
|||||||
Reference in New Issue
Block a user