Files
chatmail-core/src/download/pre_msg_metadata.rs
Simon Laux bfc58e9204 feat: receive pre-messages and adapt download on demand
fix python lint errors

receive pre-mesages, start with changes to imap loop.

refactor: move download code from `scheduler.rs` to `download.rs`, also
move `get_msg_id_by_rfc724_mid` to `MsgId::get_by_rfc724_mid`

`MAX_FETCH_MSG_SIZE` is no longer unused

Parse if it is a pre-message or full-message

start with receiving logic

get rid of `MsgId::get_by_rfc724_mid` because it was a duplicate of
`message::rfc724_mid_exists`

docs: add hint to `MimeMessage::from_bytes` stating that it has
side-effects.

receiving full message

send and receive `attachment_size` and set viewtype to text in
pre_message

metadata as struct in pre-message in header. And fill params that we can
already fill from the metadata. Also add a new api to check what
viewtype the message will have once downloaded.

api: jsonrpc: add `full_message_view_type` to `Message` and
`MessageInfo`

make PreMsgMetadata.to_header_value not consume self/PreMsgMetadata

add api to merge params

on download full message: merge new params into old params and remove
full-message metadata params

move tests to `src/tests/pre_messages.rs`

dynamically allocate test attachment bytes

fix detection of pre-messages. (it looked for the ChatFullMessageId
header in the unencrypted headers before)

fix setting dl state to avaiable on pre-messages

fix: save pre message with rfc724_mid of full message als disable
replacement for full messages

add some receiving tests and update test todo for premessage metadata

test: process full message before pre-message

test receive normal message

some serialization tests for PreMsgMetadata

remove outdated todo comment

test that pre-message contains message text

PreMsgMetadata: test_build_from_file_msg and test_build_from_file_msg

test: test_receive_pre_message_image

Test receiving the full message after receiving an edit after receiving
the pre-message

test_reaction_on_pre_message

test_full_download_after_trashed

test_webxdc_update_for_not_downloaded_instance

simplify fake webxdc generation in
test_webxdc_update_for_not_downloaded_instance

test_markseen_pre_msg

test_pre_msg_can_start_chat and test_full_msg_can_start_chat

test_download_later_keeps_message_order

test_chatlist_event_on_full_msg_download

fix download not working

log splitting into pre-message

add pre-message info to text when loading from db. this can be disabled
with config key `hide_pre_message_metadata_text` if ui wants to display
it in a prettier way.

update `download_limit` documentation

more logging: log size of pre and post messages

rename full message to Post-Message

split up the pre-message tests into multiple files

dedup test code by extracting code to create test messages into util
methods

remove post_message_view_type from api, now it is only used internally
for tests

remove `hide_pre_message_metadata_text` config option, as there
currently is no way to get the full message viewtype anymore

Update src/download.rs
resolve comment

use `parse_message_id` instead of removing `<>`parenthesis it manually

fix available_post_msgs gets no entries
handle forwarding and add a test for it.

convert comment to log warning event on unexpected download failure

add doc comment to `simple_imap_loop`

more logging

handle saving pre-message to self messages and test.
2025-12-09 14:29:17 +00:00

249 lines
8.2 KiB
Rust

use anyhow::{Context as _, Result};
use num_traits::ToPrimitive;
use serde::{Deserialize, Serialize};
use crate::context::Context;
use crate::log::warn;
use crate::message::Message;
use crate::message::Viewtype;
use crate::param::{Param, Params};
/// Metadata contained in Pre-Message that describes the Post-Message.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PreMsgMetadata {
/// size of the attachment in bytes
pub(crate) size: u64,
/// Real viewtype of message
pub(crate) viewtype: Viewtype,
/// the original file name
pub(crate) filename: String,
/// Dimensions: width and height of image or video
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) dimensions: Option<(i32, i32)>,
/// Duration of audio file or video in milliseconds
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) duration: Option<i32>,
}
impl PreMsgMetadata {
// Returns PreMsgMetadata for messages with files and None for messages without file attachment
pub(crate) async fn from_msg(context: &Context, message: &Message) -> Result<Option<Self>> {
if !message.viewtype.has_file() {
return Ok(None);
}
let size = message
.get_filebytes(context)
.await?
.context("unexpected: file has no size")?;
let filename = message
.param
.get(Param::Filename)
.unwrap_or_default()
.to_owned();
let dimensions = {
match (
message.param.get_int(Param::Width),
message.param.get_int(Param::Height),
) {
(None, None) => None,
(Some(width), Some(height)) => Some((width, height)),
_ => {
warn!(context, "Message has misses either width or height");
None
}
}
};
let duration = message.param.get_int(Param::Duration);
Ok(Some(Self {
size,
filename,
viewtype: message.viewtype,
dimensions,
duration,
}))
}
pub(crate) fn to_header_value(&self) -> Result<String> {
Ok(serde_json::to_string(&self)?)
}
pub(crate) fn try_from_header_value(value: &str) -> Result<Self> {
Ok(serde_json::from_str(value)?)
}
}
impl Params {
/// Applies data from pre_msg_metadata to Params
pub(crate) fn apply_from_pre_msg_metadata(
&mut self,
pre_msg_metadata: &PreMsgMetadata,
) -> &mut Self {
self.set(Param::PostMessageFileBytes, pre_msg_metadata.size);
if !pre_msg_metadata.filename.is_empty() {
self.set(Param::Filename, &pre_msg_metadata.filename);
}
self.set_i64(
Param::PostMessageViewtype,
pre_msg_metadata.viewtype.to_i64().unwrap_or_default(),
);
if let Some((width, height)) = pre_msg_metadata.dimensions {
self.set(Param::Width, width);
self.set(Param::Height, height);
}
if let Some(duration) = pre_msg_metadata.duration {
self.set(Param::Duration, duration);
}
self
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use pretty_assertions::assert_eq;
use crate::{
message::{Message, Viewtype},
test_utils::{TestContextManager, create_test_image},
};
use super::PreMsgMetadata;
/// Build from message with file attachment
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_build_from_file_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let mut file_msg = Message::new(Viewtype::File);
file_msg.set_file_from_bytes(alice, "test.bin", &vec![0u8; 1_000_000], None)?;
let pre_mesage_metadata = PreMsgMetadata::from_msg(alice, &file_msg).await?;
assert_eq!(
pre_mesage_metadata,
Some(PreMsgMetadata {
size: 1_000_000,
viewtype: Viewtype::File,
filename: "test.bin".to_string(),
dimensions: None,
duration: None,
})
);
Ok(())
}
/// Build from message with image attachment
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_build_from_image_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let mut image_msg = Message::new(Viewtype::Image);
let (width, height) = (1080, 1920);
let test_img = create_test_image(width, height)?;
image_msg.set_file_from_bytes(alice, "vacation.png", &test_img, None)?;
// this is usually done while sending,
// but we don't send it here, so we need to call it ourself
image_msg.try_calc_and_set_dimensions(alice).await?;
let pre_mesage_metadata = PreMsgMetadata::from_msg(alice, &image_msg).await?;
assert_eq!(
pre_mesage_metadata,
Some(PreMsgMetadata {
size: 1816098,
viewtype: Viewtype::Image,
filename: "vacation.png".to_string(),
dimensions: Some((width as i32, height as i32)),
duration: None,
})
);
Ok(())
}
/// Test that serialisation results in expected format
#[test]
fn test_serialize_to_header() -> Result<()> {
assert_eq!(
PreMsgMetadata {
size: 1_000_000,
viewtype: Viewtype::File,
filename: "test.bin".to_string(),
dimensions: None,
duration: None,
}
.to_header_value()?,
"{\"size\":1000000,\"viewtype\":\"File\",\"filename\":\"test.bin\"}"
);
assert_eq!(
PreMsgMetadata {
size: 5_342_765,
viewtype: Viewtype::Image,
filename: "vacation.png".to_string(),
dimensions: Some((1080, 1920)),
duration: None,
}
.to_header_value()?,
"{\"size\":5342765,\"viewtype\":\"Image\",\"filename\":\"vacation.png\",\"dimensions\":[1080,1920]}"
);
assert_eq!(
PreMsgMetadata {
size: 5_000,
viewtype: Viewtype::Audio,
filename: "audio-DD-MM-YY.ogg".to_string(),
dimensions: None,
duration: Some(152_310),
}
.to_header_value()?,
"{\"size\":5000,\"viewtype\":\"Audio\",\"filename\":\"audio-DD-MM-YY.ogg\",\"duration\":152310}"
);
Ok(())
}
/// Test that deserialisation from expected format works
/// This test will become important for compatibility between versions in the future
#[test]
fn test_deserialize_from_header() -> Result<()> {
assert_eq!(
serde_json::from_str::<PreMsgMetadata>(
"{\"size\":1000000,\"viewtype\":\"File\",\"filename\":\"test.bin\",\"dimensions\":null,\"duration\":null}"
)?,
PreMsgMetadata {
size: 1_000_000,
viewtype: Viewtype::File,
filename: "test.bin".to_string(),
dimensions: None,
duration: None,
}
);
assert_eq!(
serde_json::from_str::<PreMsgMetadata>(
"{\"size\":5342765,\"viewtype\":\"Image\",\"filename\":\"vacation.png\",\"dimensions\":[1080,1920]}"
)?,
PreMsgMetadata {
size: 5_342_765,
viewtype: Viewtype::Image,
filename: "vacation.png".to_string(),
dimensions: Some((1080, 1920)),
duration: None,
}
);
assert_eq!(
serde_json::from_str::<PreMsgMetadata>(
"{\"size\":5000,\"viewtype\":\"Audio\",\"filename\":\"audio-DD-MM-YY.ogg\",\"duration\":152310}"
)?,
PreMsgMetadata {
size: 5_000,
viewtype: Viewtype::Audio,
filename: "audio-DD-MM-YY.ogg".to_string(),
dimensions: None,
duration: Some(152_310),
}
);
Ok(())
}
}