Skip to content

Commit cb21578

Browse files
committed
fix: Render "message" parts in multipart messages' HTML (#4462)
This fixes the HTML display of messages containing forwarded messages. Before, forwarded messages weren't rendered in HTML and if a forwarded message is long and therefore truncated in the chat, it could only be seen in the "Message Info". In #4462 it was suggested to display "Show Full Message..." for each truncated message part and save to `msgs.mime_headers` only the corresponding part, but this is a quite huge change and refactoring and also it may be good that currently we save the full message structure to `msgs.mime_headers`, so i'd suggest not to change this for now.
1 parent 2533628 commit cb21578

File tree

2 files changed

+89
-40
lines changed

2 files changed

+89
-40
lines changed

src/html.rs

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
//! `MsgId.get_html()` will return HTML -
88
//! this allows nice quoting, handling linebreaks properly etc.
99
10+
use std::mem;
11+
1012
use anyhow::{Context as _, Result};
1113
use base64::Engine as _;
1214
use lettre_email::mime::Mime;
@@ -77,21 +79,26 @@ fn get_mime_multipart_type(ctype: &ParsedContentType) -> MimeMultipartType {
7779
struct HtmlMsgParser {
7880
pub html: String,
7981
pub plain: Option<PlainText>,
82+
pub(crate) msg_html: String,
8083
}
8184

8285
impl HtmlMsgParser {
8386
/// Function takes a raw mime-message string,
8487
/// searches for the main-text part
8588
/// and returns that as parser.html
86-
pub async fn from_bytes(context: &Context, rawmime: &[u8]) -> Result<Self> {
89+
pub async fn from_bytes<'a>(
90+
context: &Context,
91+
rawmime: &'a [u8],
92+
) -> Result<(Self, mailparse::ParsedMail<'a>)> {
8793
let mut parser = HtmlMsgParser {
8894
html: "".to_string(),
8995
plain: None,
96+
msg_html: "".to_string(),
9097
};
9198

92-
let parsedmail = mailparse::parse_mail(rawmime)?;
99+
let parsedmail = mailparse::parse_mail(rawmime).context("Failed to parse mail")?;
93100

94-
parser.collect_texts_recursive(&parsedmail).await?;
101+
parser.collect_texts_recursive(context, &parsedmail).await?;
95102

96103
if parser.html.is_empty() {
97104
if let Some(plain) = &parser.plain {
@@ -100,8 +107,8 @@ impl HtmlMsgParser {
100107
} else {
101108
parser.cid_to_data_recursive(context, &parsedmail).await?;
102109
}
103-
104-
Ok(parser)
110+
parser.html += &mem::take(&mut parser.msg_html);
111+
Ok((parser, parsedmail))
105112
}
106113

107114
/// Function iterates over all mime-parts
@@ -114,12 +121,13 @@ impl HtmlMsgParser {
114121
/// therefore we use the first one.
115122
async fn collect_texts_recursive<'a>(
116123
&'a mut self,
124+
context: &'a Context,
117125
mail: &'a mailparse::ParsedMail<'a>,
118126
) -> Result<()> {
119127
match get_mime_multipart_type(&mail.ctype) {
120128
MimeMultipartType::Multiple => {
121129
for cur_data in &mail.subparts {
122-
Box::pin(self.collect_texts_recursive(cur_data)).await?
130+
Box::pin(self.collect_texts_recursive(context, cur_data)).await?
123131
}
124132
Ok(())
125133
}
@@ -128,8 +136,35 @@ impl HtmlMsgParser {
128136
if raw.is_empty() {
129137
return Ok(());
130138
}
131-
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
132-
Box::pin(self.collect_texts_recursive(&mail)).await
139+
let (parser, mail) = Box::pin(HtmlMsgParser::from_bytes(context, &raw)).await?;
140+
if !parser.html.is_empty() {
141+
let mut text = "\r\n\r\n".to_string();
142+
for h in mail.headers {
143+
let key = h.get_key();
144+
if matches!(
145+
key.to_lowercase().as_str(),
146+
"date"
147+
| "from"
148+
| "sender"
149+
| "reply-to"
150+
| "to"
151+
| "cc"
152+
| "bcc"
153+
| "subject"
154+
) {
155+
text += &format!("{key}: {}\r\n", h.get_value());
156+
}
157+
}
158+
text += "\r\n";
159+
self.msg_html += &PlainText {
160+
text,
161+
flowed: false,
162+
delsp: false,
163+
}
164+
.to_html();
165+
self.msg_html += &parser.html;
166+
}
167+
Ok(())
133168
}
134169
MimeMultipartType::Single => {
135170
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
@@ -175,14 +210,7 @@ impl HtmlMsgParser {
175210
}
176211
Ok(())
177212
}
178-
MimeMultipartType::Message => {
179-
let raw = mail.get_body_raw()?;
180-
if raw.is_empty() {
181-
return Ok(());
182-
}
183-
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
184-
Box::pin(self.cid_to_data_recursive(context, &mail)).await
185-
}
213+
MimeMultipartType::Message => Ok(()),
186214
MimeMultipartType::Single => {
187215
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
188216
if mimetype.type_() == mime::IMAGE {
@@ -240,7 +268,7 @@ impl MsgId {
240268
warn!(context, "get_html: parser error: {:#}", err);
241269
Ok(None)
242270
}
243-
Ok(parser) => Ok(Some(parser.html)),
271+
Ok((parser, _)) => Ok(Some(parser.html)),
244272
}
245273
} else {
246274
warn!(context, "get_html: no mime for {}", self);
@@ -274,7 +302,7 @@ mod tests {
274302
async fn test_htmlparse_plain_unspecified() {
275303
let t = TestContext::new().await;
276304
let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml");
277-
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
305+
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
278306
assert_eq!(
279307
parser.html,
280308
r#"<!DOCTYPE html>
@@ -292,7 +320,7 @@ This message does not have Content-Type nor Subject.<br/>
292320
async fn test_htmlparse_plain_iso88591() {
293321
let t = TestContext::new().await;
294322
let raw = include_bytes!("../test-data/message/text_plain_iso88591.eml");
295-
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
323+
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
296324
assert_eq!(
297325
parser.html,
298326
r#"<!DOCTYPE html>
@@ -310,7 +338,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
310338
async fn test_htmlparse_plain_flowed() {
311339
let t = TestContext::new().await;
312340
let raw = include_bytes!("../test-data/message/text_plain_flowed.eml");
313-
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
341+
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
314342
assert!(parser.plain.unwrap().flowed);
315343
assert_eq!(
316344
parser.html,
@@ -332,7 +360,7 @@ and will be wrapped as usual.<br/>
332360
async fn test_htmlparse_alt_plain() {
333361
let t = TestContext::new().await;
334362
let raw = include_bytes!("../test-data/message/text_alt_plain.eml");
335-
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
363+
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
336364
assert_eq!(
337365
parser.html,
338366
r#"<!DOCTYPE html>
@@ -353,7 +381,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
353381
async fn test_htmlparse_html() {
354382
let t = TestContext::new().await;
355383
let raw = include_bytes!("../test-data/message/text_html.eml");
356-
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
384+
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
357385

358386
// on windows, `\r\n` linends are returned from mimeparser,
359387
// however, rust multiline-strings use just `\n`;
@@ -371,7 +399,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
371399
async fn test_htmlparse_alt_html() {
372400
let t = TestContext::new().await;
373401
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
374-
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
402+
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
375403
assert_eq!(
376404
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
377405
r##"<html>
@@ -386,7 +414,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
386414
async fn test_htmlparse_alt_plain_html() {
387415
let t = TestContext::new().await;
388416
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
389-
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
417+
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
390418
assert_eq!(
391419
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
392420
r##"<html>
@@ -411,7 +439,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
411439
assert!(test.find("data:").is_none());
412440

413441
// parsing converts cid: to data:
414-
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
442+
let (parser, _) = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
415443
assert!(parser.html.contains("<html>"));
416444
assert!(!parser.html.contains("Content-Id:"));
417445
assert!(parser.html.contains("data:image/jpeg;base64,/9j/4AAQ"));

src/receive_imf/tests.rs

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3834,30 +3834,51 @@ async fn test_big_forwarded_with_big_attachment() -> Result<()> {
38343834
let raw = include_bytes!("../../test-data/message/big_forwarded_with_big_attachment.eml");
38353835
let rcvd = receive_imf(t, raw, false).await?.unwrap();
38363836
assert_eq!(rcvd.msg_ids.len(), 3);
3837+
38373838
let msg = Message::load_from_db(t, rcvd.msg_ids[0]).await?;
38383839
assert_eq!(msg.get_viewtype(), Viewtype::Text);
38393840
assert_eq!(msg.get_text(), "Hello!");
3840-
// Wrong: the second bubble's text is truncated, but "Show Full Message..." is going to be shown
3841-
// in the first message bubble in the UIs.
3842-
assert_eq!(
3843-
msg.id
3844-
.get_html(t)
3845-
.await?
3846-
.unwrap()
3847-
.matches("Hello!")
3848-
.count(),
3849-
1
3850-
);
3841+
assert!(!msg.has_html());
3842+
38513843
let msg = Message::load_from_db(t, rcvd.msg_ids[1]).await?;
38523844
assert_eq!(msg.get_viewtype(), Viewtype::Text);
3853-
assert!(msg.get_text().starts_with("this text with 42 chars is just repeated."));
3845+
assert!(msg
3846+
.get_text()
3847+
.starts_with("this text with 42 chars is just repeated."));
38543848
assert!(msg.get_text().ends_with("[...]"));
3855-
// Wrong: the text is truncated, but it's not possible to see the full text in HTML.
38563849
assert!(!msg.has_html());
3850+
38573851
let msg = Message::load_from_db(t, rcvd.msg_ids[2]).await?;
38583852
assert_eq!(msg.get_viewtype(), Viewtype::File);
3859-
assert!(!msg.has_html());
3860-
3853+
assert!(msg.has_html());
3854+
let html = msg.id.get_html(t).await?.unwrap();
3855+
let tail = html
3856+
.split_once("Hello!")
3857+
.unwrap()
3858+
.1
3859+
.split_once("From: AAA")
3860+
.unwrap()
3861+
.1
3862+
.split_once("aaa@example.org")
3863+
.unwrap()
3864+
.1
3865+
.split_once("To: Alice")
3866+
.unwrap()
3867+
.1
3868+
.split_once("alice@example.org")
3869+
.unwrap()
3870+
.1
3871+
.split_once("Subject: Some subject")
3872+
.unwrap()
3873+
.1
3874+
.split_once("Date: Fri, 2 Jun 2023 12:29:17 +0000")
3875+
.unwrap()
3876+
.1;
3877+
assert_eq!(
3878+
tail.matches("this text with 42 chars is just repeated.")
3879+
.count(),
3880+
128
3881+
);
38613882
Ok(())
38623883
}
38633884

0 commit comments

Comments
 (0)