diff --git a/server/src/email_extract.rs b/server/src/email_extract.rs index 890be24..bada862 100644 --- a/server/src/email_extract.rs +++ b/server/src/email_extract.rs @@ -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,115 +642,178 @@ pub fn extract_gzip(m: &ParsedMail) -> Result<(Body, Option), ServerErro Ok((extract_unhandled(m)?, None)) } -pub fn extract_report(m: &ParsedMail, _part_addr: &mut Vec) -> Result { - let mut html_part = None; - let mut tlsrpt_part = None; +pub fn extract_report(m: &ParsedMail, part_addr: &mut Vec) -> Result { + 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 { - let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]); - let mut buffer = Vec::new(); - if decoder.read_to_end(&mut buffer).is_ok() { - if let Ok(json_str) = String::from_utf8(buffer) { - match serde_json::from_str::(&json_str) { - Ok(tlsrpt) => { - let formatted_tlsrpt = FormattedTlsRpt { - organization_name: tlsrpt.organization_name, - date_range: FormattedTlsRptDateRange { - start_datetime: tlsrpt.date_range.start_datetime, - end_datetime: tlsrpt.date_range.end_datetime, - }, - contact_info: tlsrpt.contact_info.unwrap_or_else(|| "".to_string()), - report_id: tlsrpt.report_id, - policies: tlsrpt - .policies - .into_iter() - .map(|policy| FormattedTlsRptPolicy { - policy: FormattedTlsRptPolicyDetails { - policy_type: policy.policy.policy_type, - policy_string: policy.policy.policy_string, - policy_domain: policy.policy.policy_domain, - mx_host: policy - .policy - .mx_host - .unwrap_or_else(|| Vec::new()) - .into_iter() - .map(|mx| match mx { - MxHost::String(s) => FormattedTlsRptMxHost { - hostname: s, - failure_count: 0, - result_type: "".to_string(), - }, - MxHost::Object(o) => FormattedTlsRptMxHost { - hostname: o.hostname, - failure_count: o.failure_count, - result_type: o.result_type, - }, - }) - .collect(), + 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() { + if let Ok(json_str) = String::from_utf8(buffer) { + match serde_json::from_str::(&json_str) { + Ok(tlsrpt) => { + let formatted_tlsrpt = FormattedTlsRpt { + organization_name: tlsrpt.organization_name, + date_range: FormattedTlsRptDateRange { + start_datetime: tlsrpt.date_range.start_datetime, + end_datetime: tlsrpt.date_range.end_datetime, }, - summary: policy.summary, - failure_details: policy - .failure_details - .unwrap_or_else(|| Vec::new()) + contact_info: tlsrpt + .contact_info + .unwrap_or_else(|| "".to_string()), + report_id: tlsrpt.report_id, + policies: tlsrpt + .policies .into_iter() - .map(|detail| FormattedTlsRptFailureDetails { - result_type: detail.result_type, - sending_mta_ip: detail - .sending_mta_ip - .unwrap_or_else(|| "".to_string()), - receiving_ip: detail - .receiving_ip - .unwrap_or_else(|| "".to_string()), - receiving_mx_hostname: detail - .receiving_mx_hostname - .unwrap_or_else(|| "".to_string()), - failed_session_count: detail.failed_session_count, - additional_info: detail - .additional_info - .unwrap_or_else(|| "".to_string()), - failure_reason_code: detail - .failure_reason_code - .unwrap_or_else(|| "".to_string()), + .map(|policy| FormattedTlsRptPolicy { + policy: FormattedTlsRptPolicyDetails { + policy_type: policy.policy.policy_type, + policy_string: policy.policy.policy_string, + policy_domain: policy.policy.policy_domain, + mx_host: policy + .policy + .mx_host + .unwrap_or_else(|| Vec::new()) + .into_iter() + .map(|mx| match mx { + MxHost::String(s) => { + FormattedTlsRptMxHost { + hostname: s, + failure_count: 0, + result_type: "".to_string(), + } + } + MxHost::Object(o) => { + FormattedTlsRptMxHost { + hostname: o.hostname, + failure_count: o.failure_count, + result_type: o.result_type, + } + } + }) + .collect(), + }, + summary: policy.summary, + failure_details: policy + .failure_details + .unwrap_or_else(|| Vec::new()) + .into_iter() + .map(|detail| FormattedTlsRptFailureDetails { + result_type: detail.result_type, + sending_mta_ip: detail + .sending_mta_ip + .unwrap_or_else(|| "".to_string()), + receiving_ip: detail + .receiving_ip + .unwrap_or_else(|| "".to_string()), + receiving_mx_hostname: detail + .receiving_mx_hostname + .unwrap_or_else(|| "".to_string()), + failed_session_count: detail + .failed_session_count, + additional_info: detail + .additional_info + .unwrap_or_else(|| "".to_string()), + failure_reason_code: detail + .failure_reason_code + .unwrap_or_else(|| "".to_string()), + }) + .collect(), }) .collect(), - }) - .collect(), - }; - let template = TlsReportTemplate { - report: &formatted_tlsrpt, - }; - template.render().unwrap_or_else(|e| format!("
Failed to render TLS report template: {}
", e)) + }; + let template = TlsReportTemplate { + report: &formatted_tlsrpt, + }; + let html = template.render().unwrap_or_else(|e| format!("
Failed to render TLS report template: {}
", e)); + parts.push(Body::html(html)); + } + Err(e) => { + let html = format!( + "
Failed to parse TLS report JSON: {}
", + e + ); + parts.push(Body::html(html)); + } + } + } else { + let html = format!("
Failed to convert decompressed data to UTF-8.
"); + parts.push(Body::html(html)); + } + } else { + let html = + format!("
Failed to decompress data.
"); + 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), + })); } - Err(e) => format!( - "
Failed to parse TLS report JSON: {}
", - e - ), } - } else { - format!("
Failed to convert decompressed data to UTF-8.
") } - } else { - format!("
Failed to decompressed data.
") } - } else { - "".to_string() - }; - let final_html = if let Some(html) = html_part { - format!("{}
{} ", html, tlsrpt_summary_html) - } else { - tlsrpt_summary_html - }; + part_addr.pop(); + } - Ok(Body::html(final_html)) + if parts.is_empty() { + return Ok(Body::html( + "
No report content found
".to_string(), + )); + } + + // Add
tags between subparts for better visual separation + let html = parts + .iter() + .map(|p| match p { + Body::PlainText(PlainText { text, .. }) => { + format!( + r#"

{}

"#, + linkify_html(&html_escape::encode_text(text).trim_matches('\n')) + ) + } + Body::Html(Html { html, .. }) => html.clone(), + Body::UnhandledContentType(UnhandledContentType { text, .. }) => { + format!( + r#"

{}

"#, + linkify_html(&html_escape::encode_text(text).trim_matches('\n')) + ) + } + }) + .collect::>() + .join("
\n"); + + Ok(Body::html(html)) } pub fn extract_unhandled(m: &ParsedMail) -> Result {