mirror of
https://github.com/chatmail/core.git
synced 2026-04-24 17:06:28 +03:00
add dc_msg_set_html() api (#2153)
* draft dc_msg_set_html() api * implement setting 'html to be send' * test sending html-parts * more flexible html-partbuilder * write html-parts to database and also send them * add 'sendhtml' command to repl tool
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -371,6 +371,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
send <text>\n\
|
||||
sendimage <file> [<text>]\n\
|
||||
sendfile <file> [<text>]\n\
|
||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\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?;
|
||||
|
||||
@@ -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",
|
||||
|
||||
14
src/chat.rs
14
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
|
||||
};
|
||||
|
||||
81
src/html.rs
81
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<String>) {
|
||||
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<PartBuilder> {
|
||||
self.get_html(context).await.map(|s| {
|
||||
PartBuilder::new()
|
||||
.content_type(&"text/html; charset=utf-8".parse::<mime::Mime>().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<String> {
|
||||
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::<mime::Mime>().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 <b>html</b>").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("<b>html</b> 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("<b>html</b> 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("<b>html</b> text").is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user