server: broken parsing of google ics
This commit is contained in:
parent
574de65c35
commit
c2428c073c
55
Cargo.lock
generated
55
Cargo.lock
generated
@ -2410,7 +2410,7 @@ dependencies = [
|
||||
"base64 0.21.7",
|
||||
"byteorder",
|
||||
"flate2",
|
||||
"nom",
|
||||
"nom 7.1.3",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
@ -2856,6 +2856,28 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ical"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4bad4eb99ee34e58a1e642114eded65b4ea5ea3c1584971a1afc12a3b927670"
|
||||
dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icalendar"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e69a1b52143a7fc94b55d008e8ab30506ebc93774ddb2752ee44266378fc5d6"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"iso8601",
|
||||
"nom 8.0.0",
|
||||
"nom-language",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.0.0"
|
||||
@ -3095,6 +3117,15 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "iso8601"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46"
|
||||
dependencies = [
|
||||
"nom 8.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
@ -3219,6 +3250,8 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"headers",
|
||||
"html-escape",
|
||||
"ical",
|
||||
"icalendar",
|
||||
"letterbox-notmuch 0.17.34",
|
||||
"letterbox-shared 0.17.34",
|
||||
"linkify",
|
||||
@ -3859,6 +3892,24 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom-language"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29"
|
||||
dependencies = [
|
||||
"nom 8.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
@ -6200,7 +6251,7 @@ version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e810cdeeebca57fc3f7bfec5f85fdbea9031b2ac9b990eb5ff49b371d52bbe6a"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"nom 7.1.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@ -29,6 +29,8 @@ flate2 = "1.1.2"
|
||||
futures = "0.3.31"
|
||||
headers = "0.4.0"
|
||||
html-escape = "0.2.13"
|
||||
icalendar = "0.17.1"
|
||||
ical = "0.10"
|
||||
letterbox-notmuch = { path = "../notmuch", version = "0.17.34", registry = "xinu" }
|
||||
letterbox-shared = { path = "../shared", version = "0.17.34", registry = "xinu" }
|
||||
linkify = "0.10.0"
|
||||
|
||||
@ -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>"));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user