server: add new calendar parser test
This commit is contained in:
@@ -30,6 +30,41 @@ mod tests {
|
||||
.map(|h| h.contains("ical-flex"))
|
||||
.unwrap_or(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn google_calendar_email_2_renders_ical_summary() {
|
||||
use mailparse::parse_mail;
|
||||
let raw_email = include_str!("../testdata/google-calendar-example-2.eml");
|
||||
let parsed = parse_mail(raw_email.as_bytes()).expect("parse_mail");
|
||||
let mut part_addr = vec![];
|
||||
let body = extract_body(&parsed, &mut part_addr).expect("extract_body");
|
||||
let meta = extract_calendar_metadata_from_mail(&parsed, &body);
|
||||
// Assert detection as Google Calendar
|
||||
assert!(meta.is_google_calendar_event);
|
||||
// Assert metadata extraction (update these values to match the new .eml)
|
||||
assert_eq!(meta.summary, Some("McClure BLT".to_string()));
|
||||
// Organizer: from From header, extract email address
|
||||
assert_eq!(
|
||||
meta.organizer,
|
||||
Some("calendar-notification@google.com".to_string())
|
||||
);
|
||||
// Dates: from subject, Thu Sep 11 to Fri Jan 30, 2026
|
||||
let current_year = chrono::Local::now().year();
|
||||
assert_eq!(meta.start_date, Some(format!("{}0911", current_year)));
|
||||
assert_eq!(meta.end_date, Some("20260131".to_string()));
|
||||
// Debug: print the rendered HTML for inspection
|
||||
if let Some(ref html) = meta.body_html {
|
||||
println!("Rendered HTML: {}", html);
|
||||
} else {
|
||||
println!("No body_html rendered");
|
||||
}
|
||||
// Assert ical summary is rendered and prepended (look for 'ical-flex' class)
|
||||
assert!(meta
|
||||
.body_html
|
||||
.as_ref()
|
||||
.map(|h| h.contains("ical-flex"))
|
||||
.unwrap_or(false));
|
||||
}
|
||||
}
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ExtractedCalendarMetadata {
|
||||
@@ -128,23 +163,22 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
if let Some(s) = summary_guess {
|
||||
summary = Some(s);
|
||||
}
|
||||
// Try to extract start/end dates from subject, e.g. "@ Tue Jun 24 - Mon Jun 30, 2025"
|
||||
// Try to extract start/end dates from subject
|
||||
// 1. Fallback: handle missing year, use current year for start date
|
||||
if let Some(at_idx) = subject.find('@') {
|
||||
let after_at = &subject[at_idx + 1..];
|
||||
// Look for a date range like "Tue Jun 24 - Mon Jun 30, 2025"
|
||||
let date_re = regex::Regex::new(
|
||||
r"(\w{3}) (\w{3}) (\d{1,2}) - (\w{3}) (\w{3}) (\d{1,2}), (\d{4})",
|
||||
// Try format: 'Tue Jun 24 - Mon Jun 30, 2025'
|
||||
let dash_re = regex::Regex::new(
|
||||
r"\w{3} (\w{3}) (\d{1,2}) - \w{3} (\w{3}) (\d{1,2}), (\d{4})",
|
||||
)
|
||||
.ok();
|
||||
if let Some(re) = &date_re {
|
||||
if let Some(re) = &dash_re {
|
||||
if let Some(caps) = re.captures(after_at) {
|
||||
// e.g. Tue Jun 24 - Mon Jun 30, 2025
|
||||
let start_month = &caps[2];
|
||||
let start_day = &caps[3];
|
||||
let end_month = &caps[5];
|
||||
let end_day = &caps[6];
|
||||
let year = &caps[7];
|
||||
// Try to parse months as numbers
|
||||
let start_month = &caps[1];
|
||||
let start_day = &caps[2];
|
||||
let end_month = &caps[3];
|
||||
let end_day = &caps[4];
|
||||
let year = &caps[5];
|
||||
let month_map = [
|
||||
("Jan", "01"),
|
||||
("Feb", "02"),
|
||||
@@ -177,7 +211,6 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
);
|
||||
let end_date_str =
|
||||
format!("{}{}{}", year, end_month_num, format!("{:0>2}", end_day));
|
||||
// Increment end date by one day to match iCalendar exclusive end date
|
||||
let end_date_exclusive =
|
||||
chrono::NaiveDate::parse_from_str(&end_date_str, "%Y%m%d")
|
||||
.ok()
|
||||
@@ -188,6 +221,146 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
end_date = Some(end_date_exclusive);
|
||||
}
|
||||
}
|
||||
let after_at = &subject[at_idx + 1..];
|
||||
// 1. Try regex with year in end date first
|
||||
let fallback_re = regex::Regex::new(
|
||||
r"from \w{3} (\w{3}) (\d{1,2}) to \w{3} (\w{3}) (\d{1,2}), (\d{4})",
|
||||
)
|
||||
.ok();
|
||||
let mut matched = false;
|
||||
if let Some(re) = &fallback_re {
|
||||
if let Some(caps) = re.captures(after_at) {
|
||||
let start_month = &caps[1];
|
||||
let start_day = &caps[2];
|
||||
let end_month = &caps[3];
|
||||
let end_day = &caps[4];
|
||||
let year = &caps[5];
|
||||
let current_year = chrono::Local::now().year();
|
||||
let month_map = [
|
||||
("Jan", "01"),
|
||||
("Feb", "02"),
|
||||
("Mar", "03"),
|
||||
("Apr", "04"),
|
||||
("May", "05"),
|
||||
("Jun", "06"),
|
||||
("Jul", "07"),
|
||||
("Aug", "08"),
|
||||
("Sep", "09"),
|
||||
("Oct", "10"),
|
||||
("Nov", "11"),
|
||||
("Dec", "12"),
|
||||
];
|
||||
let start_month_num = month_map
|
||||
.iter()
|
||||
.find(|(m, _)| *m == start_month)
|
||||
.map(|(_, n)| *n)
|
||||
.unwrap_or("01");
|
||||
let end_month_num = month_map
|
||||
.iter()
|
||||
.find(|(m, _)| *m == end_month)
|
||||
.map(|(_, n)| *n)
|
||||
.unwrap_or("01");
|
||||
// Use current year for start date, year from subject for end date
|
||||
let start_date_str = format!(
|
||||
"{}{}{}",
|
||||
current_year,
|
||||
start_month_num,
|
||||
format!("{:0>2}", start_day)
|
||||
);
|
||||
let end_date_str =
|
||||
format!("{}{}{}", year, end_month_num, format!("{:0>2}", end_day));
|
||||
let end_date_exclusive =
|
||||
chrono::NaiveDate::parse_from_str(&end_date_str, "%Y%m%d")
|
||||
.ok()
|
||||
.and_then(|d| d.succ_opt())
|
||||
.map(|d| d.format("%Y%m%d").to_string())
|
||||
.unwrap_or(end_date_str);
|
||||
start_date = Some(start_date_str);
|
||||
end_date = Some(end_date_exclusive);
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
// 2. If not matched, fallback to missing-year regex
|
||||
if !matched {
|
||||
let fallback_no_year_re = regex::Regex::new(
|
||||
r"from \w{3} (\w{3}) (\d{1,2}) to \w{3} (\w{3}) (\d{1,2})",
|
||||
)
|
||||
.ok();
|
||||
if let Some(re) = &fallback_no_year_re {
|
||||
if let Some(caps) = re.captures(after_at) {
|
||||
let start_month = &caps[1];
|
||||
let start_day = &caps[2];
|
||||
let end_month = &caps[3];
|
||||
let end_day = &caps[4];
|
||||
let current_year = chrono::Local::now().year();
|
||||
let month_map = [
|
||||
("Jan", "01"),
|
||||
("Feb", "02"),
|
||||
("Mar", "03"),
|
||||
("Apr", "04"),
|
||||
("May", "05"),
|
||||
("Jun", "06"),
|
||||
("Jul", "07"),
|
||||
("Aug", "08"),
|
||||
("Sep", "09"),
|
||||
("Oct", "10"),
|
||||
("Nov", "11"),
|
||||
("Dec", "12"),
|
||||
];
|
||||
let start_month_num = month_map
|
||||
.iter()
|
||||
.find(|(m, _)| *m == start_month)
|
||||
.map(|(_, n)| *n)
|
||||
.unwrap_or("01");
|
||||
let end_month_num = month_map
|
||||
.iter()
|
||||
.find(|(m, _)| *m == end_month)
|
||||
.map(|(_, n)| *n)
|
||||
.unwrap_or("01");
|
||||
let start_date_str = format!(
|
||||
"{}{}{}",
|
||||
current_year,
|
||||
start_month_num,
|
||||
format!("{:0>2}", start_day)
|
||||
);
|
||||
let end_date_str = format!(
|
||||
"{}{}{}",
|
||||
current_year,
|
||||
end_month_num,
|
||||
format!("{:0>2}", end_day)
|
||||
);
|
||||
let end_date_exclusive =
|
||||
chrono::NaiveDate::parse_from_str(&end_date_str, "%Y%m%d")
|
||||
.ok()
|
||||
.and_then(|d| d.succ_opt())
|
||||
.map(|d| d.format("%Y%m%d").to_string())
|
||||
.unwrap_or(end_date_str);
|
||||
start_date = Some(start_date_str);
|
||||
end_date = Some(end_date_exclusive);
|
||||
// Set organizer from From header if not already set
|
||||
if organizer.is_none() {
|
||||
if let Some(from) = m.headers.get_first_value("From") {
|
||||
let email = from
|
||||
.split('<')
|
||||
.nth(1)
|
||||
.and_then(|s| s.split('>').next())
|
||||
.map(|s| s.trim().to_string())
|
||||
.or_else(|| Some(from.trim().to_string()));
|
||||
organizer = email;
|
||||
}
|
||||
}
|
||||
// If we matched this, skip the other regexes
|
||||
return ExtractedCalendarMetadata {
|
||||
is_google_calendar_event: is_google,
|
||||
summary,
|
||||
organizer,
|
||||
start_date,
|
||||
end_date,
|
||||
body_html,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try to extract organizer from From header
|
||||
@@ -300,6 +473,7 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
caption,
|
||||
description_paragraphs: &description_paragraphs,
|
||||
today: Some(chrono::Local::now().date_naive()),
|
||||
recurrence_display: String::new(),
|
||||
};
|
||||
if let Ok(rendered) = template.render() {
|
||||
body_html = Some(rendered);
|
||||
@@ -1464,6 +1638,7 @@ pub struct IcalSummaryTemplate<'a> {
|
||||
pub caption: String,
|
||||
pub description_paragraphs: &'a [String],
|
||||
pub today: Option<chrono::NaiveDate>,
|
||||
pub recurrence_display: String,
|
||||
}
|
||||
|
||||
// Add this helper function to parse the DMARC XML and summarize it.
|
||||
@@ -1831,6 +2006,7 @@ pub fn render_ical_summary(ical_data: &str) -> Result<String, ServerError> {
|
||||
caption,
|
||||
description_paragraphs: description_paragraphs_val,
|
||||
today: Some(chrono::Local::now().date_naive()),
|
||||
recurrence_display: String::new(),
|
||||
};
|
||||
summary_parts.push(template.render()?);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user