diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index fae46bfc8..675ce7a75 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -3679,6 +3679,33 @@ int dc_msg_has_html (dc_msg_t* msg); void dc_msg_set_text (dc_msg_t* msg, const char* text); +/** + * Set the HTML part of a message object. + * As for all other dc_msg_t setters, + * this is only useful if the message is sent using dc_send_msg() later. + * + * Please note, that Delta Chat clients show the plain text set with + * dc_msg_set_text() at the first place; + * the HTML part is not shown instead of this text. + * However, for messages with HTML parts, + * on the receiver's device, dc_msg_has_html() will return 1 + * and a button "Show full message" is typically shown. + * + * So adding a HTML part might be useful eg. for bots, + * that want to add rich content to a message, eg. a website; + * this HTML part is similar to an attachment then. + * + * **dc_msg_set_html() is currently not meant for sending a message, + * a "normal user" has typed in!** + * Use dc_msg_set_text() for that purpose. + * + * @memberof dc_msg_t + * @param msg The message object. + * @param html HTML to send. + */ +void dc_msg_set_html (dc_msg_t* msg, const char* html); + + /** * Set the file associated with a message object. * This does not alter any information in the database diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index f54539f4a..d1b29a0cc 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -2923,6 +2923,16 @@ pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc:: ffi_msg.message.set_text(to_opt_string_lossy(text)) } +#[no_mangle] +pub unsafe extern "C" fn dc_msg_set_html(msg: *mut dc_msg_t, html: *const libc::c_char) { + if msg.is_null() { + eprintln!("ignoring careless call to dc_msg_set_html()"); + return; + } + let ffi_msg = &mut *msg; + ffi_msg.message.set_html(to_opt_string_lossy(html)) +} + #[no_mangle] pub unsafe extern "C" fn dc_msg_set_file( msg: *mut dc_msg_t, diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index 14a45cc03..8d3458ab6 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -371,6 +371,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu send \n\ sendimage []\n\ sendfile []\n\ + sendhtml []\n\ videochat\n\ draft []\n\ devicemsg \n\ @@ -833,6 +834,22 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu } chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?; } + "sendhtml" => { + ensure!(sel_chat.is_some(), "No chat selected."); + ensure!(!arg1.is_empty(), "No html-file given."); + let path: &Path = arg1.as_ref(); + let html = &*fs::read(&path)?; + let html = String::from_utf8_lossy(html); + + let mut msg = Message::new(Viewtype::Text); + msg.set_html(Some(html.to_string())); + msg.set_text(Some(if arg2.is_empty() { + path.file_name().unwrap().to_string_lossy().to_string() + } else { + arg2.to_string() + })); + chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?; + } "videochat" => { ensure!(sel_chat.is_some(), "No chat selected."); chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?; diff --git a/examples/repl/main.rs b/examples/repl/main.rs index 8a1ebb64a..d7dee34b7 100644 --- a/examples/repl/main.rs +++ b/examples/repl/main.rs @@ -168,7 +168,7 @@ const DB_COMMANDS: [&str; 9] = [ "housekeeping", ]; -const CHAT_COMMANDS: [&str; 27] = [ +const CHAT_COMMANDS: [&str; 28] = [ "listchats", "listarchived", "chat", @@ -188,6 +188,7 @@ const CHAT_COMMANDS: [&str; 27] = [ "send", "sendimage", "sendfile", + "sendhtml", "videochat", "draft", "listmedia", diff --git a/src/chat.rs b/src/chat.rs index b9adeca5c..6eb9906d1 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -30,6 +30,7 @@ use crate::dc_tools::{ }; use crate::ephemeral::{delete_expired_messages, schedule_ephemeral_task, Timer as EphemeralTimer}; use crate::events::EventType; +use crate::html::new_html_mimepart; use crate::job::{self, Action}; use crate::message::{self, InvalidMsgId, Message, MessageState, MsgId}; use crate::mimeparser::SystemMessage; @@ -1060,8 +1061,17 @@ 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 + let new_mime_headers = if msg.has_html() { + let html = if msg.param.exists(Param::Forwarded) { + msg.get_id().get_html(context).await + } else { + msg.param.get(Param::SendHtml).map(|s| s.to_string()) + }; + if let Some(html) = html { + Some(new_html_mimepart(html).await.build().as_string()) + } else { + None + } } else { None }; diff --git a/src/html.rs b/src/html.rs index 5a0285026..ece447ef8 100644 --- a/src/html.rs +++ b/src/html.rs @@ -17,6 +17,7 @@ use crate::context::Context; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::message::{Message, MsgId}; use crate::mimeparser::parse_message_id; +use crate::param::Param::SendHtml; use crate::plaintext::PlainText; use lettre_email::PartBuilder; use mailparse::ParsedContentType; @@ -31,6 +32,24 @@ impl Message { pub fn has_html(&self) -> bool { self.mime_modified } + + /// Set HTML-part part of a message that is about to be sent. + /// The HTML-part is written to the database before sending and + /// used as the `text/html` part in the MIME-structure. + /// + /// Received HTML parts are handled differently, + /// they are saved together with the whole MIME-structure + /// in `mime_headers` and the HTML-part is extracted using `MsgId::get_html()`. + /// (To underline this asynchronicity, we are using the wording "SendHtml") + pub fn set_html(&mut self, html: Option) { + if let Some(html) = html { + self.param.set(SendHtml, html); + self.mime_modified = true; + } else { + self.param.remove(SendHtml); + self.mime_modified = false; + } + } } /// Type defining a rough mime-type. @@ -250,33 +269,25 @@ impl MsgId { 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()) - } +/// Wraps HTML text into a new 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 new_html_mimepart(html: String) -> PartBuilder { + PartBuilder::new() + .content_type(&"text/html; charset=utf-8".parse::().unwrap()) + .body(html) } #[cfg(test)] mod tests { use super::*; + use crate::chat; use crate::chat::forward_msgs; use crate::config::Config; - use crate::constants::DC_CONTACT_ID_SELF; + use crate::constants::{Viewtype, DC_CONTACT_ID_SELF}; use crate::dc_receive_imf::dc_receive_imf; use crate::message::MessengerMessage; use crate::test_utils::TestContext; @@ -519,4 +530,36 @@ test some special html-characters as < > and & but also " and &#x let html = msg.get_id().get_html(&alice).await.unwrap(); assert!(html.find("this is html").is_some()); } + + #[async_std::test] + async fn test_set_html() { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // alice sends a message with html-part to bob + let chat_id = alice.create_chat(&bob).await.id; + let mut msg = Message::new(Viewtype::Text); + msg.set_text(Some("plain text".to_string())); + msg.set_html(Some("html text".to_string())); + assert!(msg.mime_modified); + chat::send_msg(&alice, chat_id, &mut msg).await.unwrap(); + + // check the message is written correctly to alice's db + let msg = alice.get_last_msg_in(chat_id).await; + assert_eq!(msg.get_text(), Some("plain text".to_string())); + assert!(!msg.is_forwarded()); + assert!(msg.mime_modified); + let html = msg.get_id().get_html(&alice).await.unwrap(); + assert!(html.find("html text").is_some()); + + // let bob receive the message + let chat_id = bob.create_chat(&alice).await.id; + bob.recv_msg(&alice.pop_sent_msg().await).await; + let msg = bob.get_last_msg_in(chat_id).await; + assert_eq!(msg.get_text(), Some("plain text".to_string())); + assert!(!msg.is_forwarded()); + assert!(msg.mime_modified); + let html = msg.get_id().get_html(&bob).await.unwrap(); + assert!(html.find("html text").is_some()); + } } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 4236ac20e..8aae58269 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -14,6 +14,7 @@ use crate::dc_tools::{ use crate::e2ee::EncryptHelper; use crate::ephemeral::Timer as EphemeralTimer; use crate::format_flowed::{format_flowed, format_flowed_quote}; +use crate::html::new_html_mimepart; use crate::location; use crate::message::{self, Message, MsgId}; use crate::mimeparser::SystemMessage; @@ -966,14 +967,16 @@ impl<'a, 'b> MimeFactory<'a, 'b> { // 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()); - } + let html = if let Some(orig_msg_id) = self.msg.param.get_int(Param::Forwarded) { + MsgId::new(orig_msg_id.try_into()?).get_html(context).await + } else { + self.msg.param.get(Param::SendHtml).map(|s| s.to_string()) + }; + if let Some(html) = html { + main_part = PartBuilder::new() + .message_type(MimeMultipartType::Alternative) + .child(main_part.build()) + .child(new_html_mimepart(html).await.build()); } } diff --git a/src/param.rs b/src/param.rs index 7dd2bdd82..0a04627be 100644 --- a/src/param.rs +++ b/src/param.rs @@ -34,6 +34,11 @@ pub enum Param { /// For Messages MimeType = b'm', + /// For Messages: HTML to be written to the database and to be send. + /// `SendHtml` param is not used for received messages. + /// Use `MsgId::get_html()` to get HTML of received messages. + SendHtml = b'T', + /// For Messages: message is encrypted, outgoing: guarantee E2EE or the message is not send GuaranteeE2ee = b'c',