server: broken parsing of google ics
This commit is contained in:
@@ -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>"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user