server: add handling for multipart/report w/ ract_rfc822 subpart

This commit is contained in:
Bill Thiede 2025-10-20 19:44:04 -07:00
parent 285ff1d098
commit 1537333e76

View File

@ -17,6 +17,7 @@ use crate::{
const APPLICATION_GZIP: &'static str = "application/gzip";
const APPLICATION_ZIP: &'static str = "application/zip";
const APPLICATION_TLSRPT_GZIP: &'static str = "application/tlsrpt+gzip";
const IMAGE_JPEG: &'static str = "image/jpeg";
const IMAGE_PJPEG: &'static str = "image/pjpeg";
const IMAGE_PNG: &'static str = "image/png";
@ -641,19 +642,15 @@ pub fn extract_gzip(m: &ParsedMail) -> Result<(Body, Option<String>), ServerErro
Ok((extract_unhandled(m)?, None))
}
pub fn extract_report(m: &ParsedMail, _part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
let mut html_part = None;
let mut tlsrpt_part = None;
pub fn extract_report(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
let mut parts = Vec::new();
for (idx, sp) in m.subparts.iter().enumerate() {
part_addr.push(idx.to_string());
for sp in &m.subparts {
match sp.ctype.mimetype.as_str() {
TEXT_HTML => html_part = Some(sp.get_body()?),
"application/tlsrpt+gzip" => tlsrpt_part = Some(sp.get_body_raw()?),
_ => {} // Ignore other parts for now
}
}
let tlsrpt_summary_html = if let Some(gz_bytes) = tlsrpt_part {
APPLICATION_TLSRPT_GZIP => {
let gz_bytes = sp.get_body_raw()?;
let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]);
let mut buffer = Vec::new();
if decoder.read_to_end(&mut buffer).is_ok() {
@ -666,7 +663,9 @@ pub fn extract_report(m: &ParsedMail, _part_addr: &mut Vec<String>) -> Result<Bo
start_datetime: tlsrpt.date_range.start_datetime,
end_datetime: tlsrpt.date_range.end_datetime,
},
contact_info: tlsrpt.contact_info.unwrap_or_else(|| "".to_string()),
contact_info: tlsrpt
.contact_info
.unwrap_or_else(|| "".to_string()),
report_id: tlsrpt.report_id,
policies: tlsrpt
.policies
@ -682,16 +681,20 @@ pub fn extract_report(m: &ParsedMail, _part_addr: &mut Vec<String>) -> Result<Bo
.unwrap_or_else(|| Vec::new())
.into_iter()
.map(|mx| match mx {
MxHost::String(s) => FormattedTlsRptMxHost {
MxHost::String(s) => {
FormattedTlsRptMxHost {
hostname: s,
failure_count: 0,
result_type: "".to_string(),
},
MxHost::Object(o) => FormattedTlsRptMxHost {
}
}
MxHost::Object(o) => {
FormattedTlsRptMxHost {
hostname: o.hostname,
failure_count: o.failure_count,
result_type: o.result_type,
},
}
}
})
.collect(),
},
@ -711,7 +714,8 @@ pub fn extract_report(m: &ParsedMail, _part_addr: &mut Vec<String>) -> Result<Bo
receiving_mx_hostname: detail
.receiving_mx_hostname
.unwrap_or_else(|| "".to_string()),
failed_session_count: detail.failed_session_count,
failed_session_count: detail
.failed_session_count,
additional_info: detail
.additional_info
.unwrap_or_else(|| "".to_string()),
@ -726,30 +730,90 @@ pub fn extract_report(m: &ParsedMail, _part_addr: &mut Vec<String>) -> Result<Bo
let template = TlsReportTemplate {
report: &formatted_tlsrpt,
};
template.render().unwrap_or_else(|e| format!("<div class=\"tlsrpt-error\">Failed to render TLS report template: {}</div>", e))
let html = template.render().unwrap_or_else(|e| format!("<div class=\"tlsrpt-error\">Failed to render TLS report template: {}</div>", e));
parts.push(Body::html(html));
}
Err(e) => format!(
Err(e) => {
let html = format!(
"<div class=\"tlsrpt-error\">Failed to parse TLS report JSON: {}</div>",
e
),
);
parts.push(Body::html(html));
}
}
} else {
format!("<div class=\"tlsrpt-error\">Failed to convert decompressed data to UTF-8.</div>")
let html = format!("<div class=\"tlsrpt-error\">Failed to convert decompressed data to UTF-8.</div>");
parts.push(Body::html(html));
}
} else {
format!("<div class=\"tlsrpt-error\">Failed to decompressed data.</div>")
let html =
format!("<div class=\"tlsrpt-error\">Failed to decompress data.</div>");
parts.push(Body::html(html));
}
}
MESSAGE_RFC822 => {
parts.push(extract_rfc822(&sp, part_addr)?);
}
TEXT_HTML => {
let body = sp.get_body()?;
parts.push(Body::html(body));
}
TEXT_PLAIN => {
let body = sp.get_body()?;
parts.push(Body::text(body));
}
_ => {
// For any other content type, try to extract the body using the general extract_body function
match extract_body(sp, part_addr) {
Ok(body) => parts.push(body),
Err(_) => {
// If extraction fails, create an unhandled content type body
let msg = format!(
"Unhandled report subpart content type: {}\n{}",
sp.ctype.mimetype,
sp.get_body()
.unwrap_or_else(|_| "Failed to get body".to_string())
);
parts.push(Body::UnhandledContentType(UnhandledContentType {
text: msg,
content_tree: render_content_type_tree(sp),
}));
}
}
}
}
} else {
"".to_string()
};
let final_html = if let Some(html) = html_part {
format!("{}<hr>{} ", html, tlsrpt_summary_html)
} else {
tlsrpt_summary_html
};
part_addr.pop();
}
Ok(Body::html(final_html))
if parts.is_empty() {
return Ok(Body::html(
"<div class=\"report-error\">No report content found</div>".to_string(),
));
}
// Add <hr> tags between subparts for better visual separation
let html = parts
.iter()
.map(|p| match p {
Body::PlainText(PlainText { text, .. }) => {
format!(
r#"<p class="view-part-text-plain font-mono whitespace-pre-line">{}</p>"#,
linkify_html(&html_escape::encode_text(text).trim_matches('\n'))
)
}
Body::Html(Html { html, .. }) => html.clone(),
Body::UnhandledContentType(UnhandledContentType { text, .. }) => {
format!(
r#"<p class="view-part-unhandled">{}</p>"#,
linkify_html(&html_escape::encode_text(text).trim_matches('\n'))
)
}
})
.collect::<Vec<_>>()
.join("<hr>\n");
Ok(Body::html(html))
}
pub fn extract_unhandled(m: &ParsedMail) -> Result<Body, ServerError> {