server: broken parsing of google ics

This commit is contained in:
2025-08-19 09:51:58 -07:00
parent 574de65c35
commit c2428c073c
3 changed files with 155 additions and 5 deletions

View File

@@ -24,6 +24,7 @@ const MULTIPART_ALTERNATIVE: &'static str = "multipart/alternative";
const MULTIPART_MIXED: &'static str = "multipart/mixed";
const MULTIPART_RELATED: &'static str = "multipart/related";
const MULTIPART_REPORT: &'static str = "multipart/report";
const TEXT_CALENDAR: &'static str = "text/calendar";
const TEXT_HTML: &'static str = "text/html";
const TEXT_PLAIN: &'static str = "text/plain";
@@ -308,6 +309,7 @@ pub fn extract_alternative(
MULTIPART_ALTERNATIVE,
MULTIPART_MIXED,
MULTIPART_RELATED,
TEXT_CALENDAR,
TEXT_HTML,
TEXT_PLAIN,
];
@@ -332,6 +334,13 @@ pub fn extract_alternative(
return Ok(Body::html(body));
}
}
for sp in &m.subparts {
if sp.ctype.mimetype.as_str() == TEXT_CALENDAR {
let body = sp.get_body()?;
let summary = render_ical_summary(&body)?;
return Ok(Body::html(summary));
}
}
for sp in &m.subparts {
if sp.ctype.mimetype.as_str() == TEXT_PLAIN {
let body = sp.get_body()?;
@@ -414,9 +423,9 @@ pub fn extract_mixed(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body
// For DMARC, it's always XML.
// Pretty print JSON (if it were TLS)
if let Ok(parsed_json) = serde_json::from_str::<serde_json::Value>(&xml) {
serde_json::to_string_pretty(&parsed_json).unwrap_or(xml)
serde_json::to_string_pretty(&parsed_json).unwrap_or(xml.to_string())
} else {
xml
xml.to_string()
}
} else {
// DMARC reports are XML
@@ -425,7 +434,7 @@ pub fn extract_mixed(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body
Ok(pretty_xml) => pretty_xml,
Err(e) => {
error!("Failed to pretty print XML: {:?}", e);
xml
xml.to_string()
}
}
};
@@ -1263,6 +1272,73 @@ pub fn pretty_print_xml_with_trimming(xml_input: &str) -> Result<String, ServerE
Ok(String::from_utf8(result)?)
}
use ical::IcalParser;
pub fn render_ical_summary(ical_data: &str) -> Result<String, ServerError> {
let mut summary_parts = Vec::new();
let mut parser = IcalParser::new(ical_data.as_bytes());
while let Some(Ok(calendar)) = parser.next() {
for event in calendar.events {
let mut event_summary = String::new();
let mut summary = None;
let mut dtstart = None;
let mut dtend = None;
let mut organizer = None;
let mut organizer_cn = None;
for prop in &event.properties {
match prop.name.as_str() {
"SUMMARY" => summary = prop.value.as_deref(),
"DTSTART" => dtstart = prop.value.as_deref(),
"DTEND" => dtend = prop.value.as_deref(),
"ORGANIZER" => {
organizer = prop.value.as_deref();
if let Some(params) = &prop.params {
if let Some((_, values)) = params.iter().find(|(k, _)| k == "CN") {
if let Some(cn) = values.get(0) {
organizer_cn = Some(cn.as_str());
}
}
}
},
_ => {}
}
}
if let Some(summary) = summary {
event_summary.push_str(&format!("<b>Summary:</b> {}<br>", summary));
}
if let Some(dtstart) = dtstart {
let formatted = parse_ical_datetime(dtstart).unwrap_or_else(|| dtstart.to_string());
event_summary.push_str(&format!("<b>Start:</b> {}<br>", formatted));
}
if let Some(dtend) = dtend {
let formatted = parse_ical_datetime(dtend).unwrap_or_else(|| dtend.to_string());
event_summary.push_str(&format!("<b>End:</b> {}<br>", formatted));
}
if let Some(cn) = organizer_cn {
event_summary.push_str(&format!("<b>Organizer:</b> {}<br>", cn));
} else if let Some(organizer) = organizer {
event_summary.push_str(&format!("<b>Organizer:</b> {}<br>", organizer));
}
summary_parts.push(event_summary);
}
}
fn parse_ical_datetime(dt: &str) -> Option<String> {
use chrono::{NaiveDateTime, DateTime, Utc};
let dt = dt.split(':').last().unwrap_or(dt);
if let Ok(ndt) = NaiveDateTime::parse_from_str(dt, "%Y%m%dT%H%M%SZ") {
let dt_utc: DateTime<Utc> = DateTime::from_utc(ndt, Utc);
return Some(dt_utc.format("%Y-%m-%dT%H:%M:%SZ").to_string());
}
if let Ok(ndt) = NaiveDateTime::parse_from_str(dt, "%Y%m%dT%H%M%S") {
return Some(ndt.format("%Y-%m-%dT%H:%M:%S").to_string());
}
None
}
Ok(summary_parts.join("<hr>"))
}
#[cfg(test)]
mod tests {
use std::fs;
@@ -1283,4 +1359,25 @@ mod tests {
let html = parse_dmarc_report(&xml).unwrap();
assert!(!html.contains("Envelope To"));
}
#[test]
fn test_ical_render() {
let ical = fs::read_to_string("testdata/ical-example-1.ics").unwrap();
let html = render_ical_summary(&ical).unwrap();
assert!(html.contains("<b>Summary:</b> dentist night guard<br>"));
assert!(html.contains("<b>Start:</b> 2025-01-08T08:00:00<br>"));
assert!(html.contains("<b>End:</b> 2025-01-08T09:00:00<br>"));
assert!(html.contains("<b>Organizer:</b> Bill Thiede<br>"));
}
#[test]
fn test_ical_render_2() {
let ical = fs::read_to_string("testdata/ical-example-2.ics").unwrap();
let html = render_ical_summary(&ical).unwrap();
println!("HTML OUTPUT: {}", html);
assert!(html.contains("<b>Summary:</b> [tenative] dinner w/ amatute<br>"));
assert!(html.contains("<b>Start:</b> 2025-08-13T01:00:00Z<br>"));
assert!(html.contains("<b>End:</b> 2025-08-13T03:00:00Z<br>"));
assert!(html.contains("<b>Organizer:</b> Family<br>"));
}
}