diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index 03ab5a681..40c9c6258 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -951,7 +951,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu let file = dirs::home_dir() .unwrap_or_default() .join(format!("msg-{}.html", id.to_u32())); - let html = id.get_html(&context).await; + let html = id.get_html(&context).await.unwrap_or_default(); fs::write(&file, html)?; println!("HTML written to: {:#?}", file); } diff --git a/src/chat.rs b/src/chat.rs index 088668619..476706a90 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1060,6 +1060,12 @@ impl Chat { EphemeralTimer::Enabled { duration } => time() + i64::from(duration), }; + let new_mime_headers = if msg.param.exists(Param::Forwarded) && msg.mime_modified { + msg.get_id().get_html_as_rawmime(context).await + } else { + None + }; + // add message to the database if context @@ -1078,10 +1084,12 @@ impl Chat { hidden, mime_in_reply_to, mime_references, + mime_modified, + mime_headers, location_id, ephemeral_timer, ephemeral_timestamp) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);", + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);", paramsv![ new_rfc724_mid, self.id, @@ -1095,6 +1103,8 @@ impl Chat { msg.hidden, msg.in_reply_to.as_deref().unwrap_or_default(), new_references, + new_mime_headers.is_some(), + new_mime_headers, location_id as i32, ephemeral_timer, ephemeral_timestamp @@ -2738,7 +2748,8 @@ pub async fn forward_msgs( // we tested a sort of broadcast // by not marking own forwarded messages as such, // however, this turned out to be to confusing and unclear. - msg.param.set_int(Param::Forwarded, 1); + msg.param + .set_int(Param::Forwarded, src_msg_id.to_u32() as i32); msg.param.remove(Param::GuaranteeE2ee); msg.param.remove(Param::ForcePlaintext); diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index 8869a4455..f3e281e87 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -822,16 +822,16 @@ async fn add_parts( // if indicated by the parser, // we save the full mime-message and add a flag // that the ui should show button to display the full message. - // - // (currently, we skip saving mime-messages for encrypted messages - // as there is probably no huge intersection between html-messages and encrypted messages, - // however, that should be doable, we need the decrypted mime-structure in this case) // a flag used to avoid adding "show full message" button to multiple parts of the message. - let mut save_mime_modified = mime_parser.is_mime_modified && !mime_parser.was_encrypted(); + let mut save_mime_modified = mime_parser.is_mime_modified; let mime_headers = if save_mime_headers || save_mime_modified { - Some(String::from_utf8_lossy(imf_raw).to_string()) + if mime_parser.was_encrypted() { + Some(String::from_utf8_lossy(&mime_parser.decoded_data).to_string()) + } else { + Some(String::from_utf8_lossy(imf_raw).to_string()) + } } else { None }; diff --git a/src/html.rs b/src/html.rs index 2c3c45bc9..f39913dc2 100644 --- a/src/html.rs +++ b/src/html.rs @@ -18,6 +18,7 @@ use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::message::{Message, MsgId}; use crate::mimeparser::parse_message_id; use crate::plaintext::PlainText; +use lettre_email::PartBuilder; use mailparse::ParsedContentType; impl Message { @@ -221,7 +222,7 @@ impl MsgId { /// this is the case at least when `Message.has_html()` returns true /// (we do not save raw mime unconditionally in the database to save space). /// The corresponding ffi-function is `dc_get_msg_html()`. - pub async fn get_html(self, context: &Context) -> String { + pub async fn get_html(self, context: &Context) -> Option { let rawmime: Option = context .sql .query_get_value( @@ -232,19 +233,52 @@ impl MsgId { .await; if let Some(rawmime) = rawmime { - match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await { - Err(err) => format!("parser error: {}", err), - Ok(parser) => parser.html, + if !rawmime.is_empty() { + match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await { + Err(err) => { + warn!(context, "get_html: parser error: {}", err); + None + } + Ok(parser) => Some(parser.html), + } + } else { + warn!(context, "get_html: empty mime for {}", self); + None } } else { - format!("parser error: no mime for {}", self) + warn!(context, "get_html: no mime for {}", self); + None } } + + /// Wraps HTML generated by [`MsgId::get_html`] into a text/html mimepart structure. + /// + /// Used on forwarding messages to avoid leaking the original mime structure + /// and also to avoid sending too much, maybe large data. + pub async fn get_html_as_mimepart(self, context: &Context) -> Option { + self.get_html(context).await.map(|s| { + PartBuilder::new() + .content_type(&"text/html; charset=utf-8".parse::().unwrap()) + .body(s) + }) + } + + // As [`MsgId::get_html_as_mimepart`] but wraps [`MsgId::get_html`] into text/html mime raw string. + pub async fn get_html_as_rawmime(self, context: &Context) -> Option { + self.get_html_as_mimepart(context) + .await + .map(|p| p.build().as_string()) + } } #[cfg(test)] mod tests { use super::*; + use crate::chat::forward_msgs; + use crate::config::Config; + use crate::constants::DC_CONTACT_ID_SELF; + use crate::dc_receive_imf::dc_receive_imf; + use crate::message::MessengerMessage; use crate::test_utils::TestContext; #[async_std::test] @@ -393,4 +427,96 @@ test some special html-characters as < > and & but also " and &#x .is_some()); assert!(parser.html.find("cid:").is_none()); } + + #[async_std::test] + async fn test_get_html_empty() { + let t = TestContext::new().await; + let msg_id = MsgId::new_unset(); + assert!(msg_id.get_html(&t).await.is_none()) + } + + #[async_std::test] + async fn test_html_forwarding() { + // alice receives a non-delta html-message + let alice = TestContext::new_alice().await; + alice.set_config(Config::ShowEmails, Some("2")).await.ok(); + let chat = alice.chat_with_contact("", "sender@testrun.org").await; + let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml"); + dc_receive_imf(&alice, raw, "INBOX", 1, false) + .await + .unwrap(); + let msg = alice.get_last_msg_in(chat.get_id()).await; + assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF); + assert_eq!(msg.is_dc_message, MessengerMessage::No); + assert!(!msg.is_forwarded()); + assert!(msg.get_text().unwrap().find("this is plain").is_some()); + assert!(msg.has_html()); + let html = msg.get_id().get_html(&alice).await.unwrap(); + assert!(html.find("this is html").is_some()); + + // alice: create chat with bob and forward received html-message there + let chat = alice.chat_with_contact("", "bob@example.net").await; + forward_msgs(&alice, &[msg.get_id()], chat.get_id()) + .await + .unwrap(); + let msg = alice.get_last_msg_in(chat.get_id()).await; + assert_eq!(msg.get_from_id(), DC_CONTACT_ID_SELF); + assert_eq!(msg.is_dc_message, MessengerMessage::Yes); + assert!(msg.is_forwarded()); + assert!(msg.get_text().unwrap().find("this is plain").is_some()); + assert!(msg.has_html()); + let html = msg.get_id().get_html(&alice).await.unwrap(); + assert!(html.find("this is html").is_some()); + + // bob: check that bob also got the html-part of the forwarded message + let bob = TestContext::new_bob().await; + let chat = bob.chat_with_contact("", "alice@example.com").await; + bob.recv_msg(&alice.pop_sent_msg().await).await; + let msg = bob.get_last_msg_in(chat.get_id()).await; + assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF); + assert_eq!(msg.is_dc_message, MessengerMessage::Yes); + assert!(msg.is_forwarded()); + assert!(msg.get_text().unwrap().find("this is plain").is_some()); + assert!(msg.has_html()); + let html = msg.get_id().get_html(&bob).await.unwrap(); + assert!(html.find("this is html").is_some()); + } + + #[async_std::test] + async fn test_html_forwarding_encrypted() { + // Alice receives a non-delta html-message + // (`ShowEmails=1` lets Alice actually receive non-delta messages for known contacts, + // the contact is marked as known by creating a chat using `chat_with_contact()`) + let alice = TestContext::new_alice().await; + alice.set_config(Config::ShowEmails, Some("1")).await.ok(); + let chat = alice.chat_with_contact("", "sender@testrun.org").await; + let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml"); + dc_receive_imf(&alice, raw, "INBOX", 1, false) + .await + .unwrap(); + let msg = alice.get_last_msg_in(chat.get_id()).await; + + // forward the message to saved-messages, + // this will encrypt the message as new_alice() has set up keys + let chat = alice.get_self_chat().await; + forward_msgs(&alice, &[msg.get_id()], chat.get_id()) + .await + .unwrap(); + let msg = alice.pop_sent_msg().await; + + // receive the message on another device + let alice = TestContext::new_alice().await; + assert_eq!(alice.get_config_int(Config::ShowEmails).await, 0); // set to "1" above, make sure it is another db + alice.recv_msg(&msg).await; + let chat = alice.get_self_chat().await; + let msg = alice.get_last_msg_in(chat.get_id()).await; + assert_eq!(msg.get_from_id(), DC_CONTACT_ID_SELF); + assert_eq!(msg.is_dc_message, MessengerMessage::Yes); + assert!(msg.get_showpadlock()); + assert!(msg.is_forwarded()); + assert!(msg.get_text().unwrap().find("this is plain").is_some()); + assert!(msg.has_html()); + let html = msg.get_id().get_html(&alice).await.unwrap(); + assert!(html.find("this is html").is_some()); + } } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index ed1efc98e..2e51dc331 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -15,12 +15,13 @@ use crate::ephemeral::Timer as EphemeralTimer; use crate::error::{bail, ensure, format_err, Error}; use crate::format_flowed::{format_flowed, format_flowed_quote}; use crate::location; -use crate::message::{self, Message}; +use crate::message::{self, Message, MsgId}; use crate::mimeparser::SystemMessage; use crate::param::Param; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::simplify::escape_message_footer_marks; use crate::stock::StockMessage; +use std::convert::TryInto; // attachments of 25 mb brutto should work on the majority of providers // (brutto examples: web.de=50, 1&1=40, t-online.de=32, gmail=25, posteo=50, yahoo=25, all-inkl=100). @@ -954,7 +955,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { ); // Message is sent as text/plain, with charset = utf-8 - let main_part = PartBuilder::new() + let mut main_part = PartBuilder::new() .header(( "Content-Type".to_string(), "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(), @@ -962,6 +963,20 @@ impl<'a, 'b> MimeFactory<'a, 'b> { .body(message_text); let mut parts = Vec::new(); + // add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded; + // for simplificity and to avoid conversion errors, we're generating the HTML-part from the original message. + if self.msg.has_html() { + if let Some(orig_msg_id) = self.msg.param.get_int(Param::Forwarded) { + let orig_msg_id = MsgId::new(orig_msg_id.try_into()?); + if let Some(html_part) = orig_msg_id.get_html_as_mimepart(context).await { + main_part = PartBuilder::new() + .message_type(MimeMultipartType::Alternative) + .child(main_part.build()) + .child(html_part.build()); + } + } + } + // add attachment part if chat::msgtype_has_file(self.msg.viewtype) { if !is_file_size_okay(context, &self.msg).await { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 2443252e3..36670126b 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -69,6 +69,12 @@ pub struct MimeMessage { // if this flag is set, the parts/text/etc. are just close to the original mime-message; // clients should offer a way to view the original message in this case pub is_mime_modified: bool, + + /// The decrypted, raw mime structure. + /// + /// This is non-empty only if the message was actually encrypted. It is used + /// for e.g. late-parsing HTML. + pub decoded_data: Vec, } #[derive(Debug, PartialEq)] @@ -136,7 +142,7 @@ impl MimeMessage { headers.remove("chat-verified"); // Memory location for a possible decrypted message. - let mail_raw; + let mut mail_raw = Vec::new(); let mut gossipped_addr = Default::default(); let (mail, signatures, warn_empty_signature) = @@ -228,6 +234,7 @@ impl MimeMessage { group_avatar: None, failure_report: None, is_mime_modified: false, + decoded_data: Vec::new(), }; parser.parse_mime_recursive(context, &mail).await?; parser.maybe_remove_bad_parts(); @@ -240,6 +247,10 @@ impl MimeMessage { } } + if parser.is_mime_modified { + parser.decoded_data = mail_raw; + } + Ok(parser) } diff --git a/src/param.rs b/src/param.rs index 9e0f5cffb..329bf8b47 100644 --- a/src/param.rs +++ b/src/param.rs @@ -50,7 +50,8 @@ pub enum Param { /// For Messages WantsMdn = b'r', - /// For Messages + /// For Messages: unset or 0=not forwarded, + /// 1=forwarded from unknown msg_id, >9 forwarded from msg_id Forwarded = b'a', /// For Messages: quoted text. diff --git a/test-data/message/text_alt_plain_html.eml b/test-data/message/text_alt_plain_html.eml index 596499778..13f20c7d8 100644 --- a/test-data/message/text_alt_plain_html.eml +++ b/test-data/message/text_alt_plain_html.eml @@ -1,7 +1,7 @@ Subject: mime-modified test Message-ID: 12345@testrun.org Date: Sat, 07 Dec 2019 19:00:27 +0000 -To: recp@testrun.org +To: alice@example.com From: sender@testrun.org Content-Type: multipart/alternative; boundary="==BREAK=="