server: big improvements for parsing all day events
This commit is contained in:
@@ -241,6 +241,31 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
if end_date.is_none() { end_date = Some(end); }
|
||||
}
|
||||
}
|
||||
// Pattern: single all-day event: @ Sun Jan 18, 2026 (no time range)
|
||||
if start_date.is_none() {
|
||||
if let Some(caps) = regex::Regex::new(r"@ [A-Za-z]{3} ([A-Za-z]{3}) (\d{1,2}), (\d{4})(?:\s*\(|$)").ok().and_then(|re| re.captures(&subject)) {
|
||||
let month = &caps[1];
|
||||
let day = &caps[2];
|
||||
let year = &caps[3];
|
||||
fn month_num(mon: &str) -> Option<&'static str> {
|
||||
match mon {
|
||||
"Jan" => Some("01"), "Feb" => Some("02"), "Mar" => Some("03"), "Apr" => Some("04"),
|
||||
"May" => Some("05"), "Jun" => Some("06"), "Jul" => Some("07"), "Aug" => Some("08"),
|
||||
"Sep" => Some("09"), "Oct" => Some("10"), "Nov" => Some("11"), "Dec" => Some("12"),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
if let Some(mm) = month_num(month) {
|
||||
let start = format!("{}{}{:0>2}", year, mm, day);
|
||||
// For all-day events, end date is the next day (exclusive)
|
||||
if let Ok(d) = chrono::NaiveDate::parse_from_str(&format!("{}-{}-{:0>2}", year, mm, day), "%Y-%m-%d") {
|
||||
let end = d.succ_opt().unwrap_or(d).format("%Y%m%d").to_string();
|
||||
start_date = Some(start);
|
||||
end_date = Some(end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,6 +353,8 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
let needs_ical_flex =
|
||||
summary.is_some() || start_date.is_some() || end_date.is_some() || has_recurrence;
|
||||
if needs_ical_flex {
|
||||
use chrono::{Datelike, NaiveDate};
|
||||
|
||||
let summary_val = summary.clone().unwrap_or_default();
|
||||
let organizer_val = organizer.clone().unwrap_or_default();
|
||||
let start_val = start_date.clone().unwrap_or_default();
|
||||
@@ -337,15 +364,101 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// Compute event_days and all_days for calendar grid rendering
|
||||
let mut event_days: Vec<NaiveDate> = Vec::new();
|
||||
let (local_fmt_start, local_fmt_end) = if let (Some(ref start_str), Some(ref end_str)) =
|
||||
(&start_date, &end_date)
|
||||
{
|
||||
// Parse YYYYMMDD format dates
|
||||
let start_d = NaiveDate::parse_from_str(start_str, "%Y%m%d").ok();
|
||||
let end_d = NaiveDate::parse_from_str(end_str, "%Y%m%d").ok();
|
||||
|
||||
if let (Some(start), Some(end)) = (start_d, end_d) {
|
||||
// For all-day events, end date is exclusive, so we need to subtract one day
|
||||
let display_end = if end > start {
|
||||
end.pred_opt().unwrap_or(end)
|
||||
} else {
|
||||
end
|
||||
};
|
||||
|
||||
// Add all days from start to display_end (inclusive) to event_days
|
||||
let mut day_iter = start;
|
||||
while day_iter <= display_end {
|
||||
event_days.push(day_iter);
|
||||
day_iter = day_iter.succ_opt().unwrap_or(day_iter);
|
||||
if day_iter == display_end && day_iter == start {
|
||||
// Single day event
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Format dates for display
|
||||
let fmt_start = start.format("%a %b %e, %Y").to_string();
|
||||
let fmt_end = display_end.format("%a %b %e, %Y").to_string();
|
||||
(fmt_start, fmt_end)
|
||||
} else {
|
||||
(start_val.clone(), end_val.clone())
|
||||
}
|
||||
} else {
|
||||
(start_val.clone(), end_val.clone())
|
||||
};
|
||||
|
||||
// Compute calendar grid (all_days) from event_days
|
||||
let (all_days, caption) = if !event_days.is_empty() {
|
||||
let first_event = event_days.first().unwrap();
|
||||
let last_event = event_days.last().unwrap();
|
||||
|
||||
let first_of_month =
|
||||
NaiveDate::from_ymd_opt(first_event.year(), first_event.month(), 1).unwrap();
|
||||
let last_of_month = {
|
||||
let next_month = if last_event.month() == 12 {
|
||||
NaiveDate::from_ymd_opt(last_event.year() + 1, 1, 1).unwrap()
|
||||
} else {
|
||||
NaiveDate::from_ymd_opt(last_event.year(), last_event.month() + 1, 1).unwrap()
|
||||
};
|
||||
next_month.pred_opt().unwrap()
|
||||
};
|
||||
|
||||
// Start from Sunday of the week containing first_of_month
|
||||
let mut cal_start = first_of_month;
|
||||
while cal_start.weekday() != chrono::Weekday::Sun {
|
||||
cal_start = cal_start.pred_opt().unwrap();
|
||||
}
|
||||
// End on Saturday of the week containing last_of_month
|
||||
let mut cal_end = last_of_month;
|
||||
while cal_end.weekday() != chrono::Weekday::Sat {
|
||||
cal_end = cal_end.succ_opt().unwrap();
|
||||
}
|
||||
|
||||
let mut all_days = vec![];
|
||||
let mut d = cal_start;
|
||||
while d <= cal_end {
|
||||
all_days.push(d);
|
||||
d = d.succ_opt().unwrap();
|
||||
}
|
||||
|
||||
let start_month = first_event.format("%B %Y");
|
||||
let end_month = last_event.format("%B %Y");
|
||||
let caption = if start_month.to_string() == end_month.to_string() {
|
||||
start_month.to_string()
|
||||
} else {
|
||||
format!("{} – {}", start_month, end_month)
|
||||
};
|
||||
(all_days, caption)
|
||||
} else {
|
||||
(vec![], String::new())
|
||||
};
|
||||
|
||||
let template = IcalSummaryTemplate {
|
||||
summary: &summary_val,
|
||||
local_fmt_start: &start_val,
|
||||
local_fmt_end: &end_val,
|
||||
local_fmt_start: &local_fmt_start,
|
||||
local_fmt_end: &local_fmt_end,
|
||||
organizer: &organizer_val,
|
||||
organizer_cn: "",
|
||||
all_days: vec![],
|
||||
event_days: vec![],
|
||||
caption: String::new(),
|
||||
all_days,
|
||||
event_days,
|
||||
caption,
|
||||
description_paragraphs: &[],
|
||||
today: Some(chrono::Local::now().date_naive()),
|
||||
recurrence_display,
|
||||
@@ -2286,8 +2399,9 @@ mod tests {
|
||||
html.contains("Dentist appt"),
|
||||
"HTML should contain the summary"
|
||||
);
|
||||
// Date is now formatted as human-readable "Tue Sep 23, 2025"
|
||||
assert!(
|
||||
html.contains("20250923"),
|
||||
html.contains("Sep 23, 2025") || html.contains("20250923"),
|
||||
"HTML should contain the event date"
|
||||
);
|
||||
assert!(
|
||||
@@ -2341,12 +2455,13 @@ mod tests {
|
||||
html.contains("<b>Organizer:</b> calendar-notification@google.com"),
|
||||
"HTML should contain the labeled organizer"
|
||||
);
|
||||
// Dates are now formatted as human-readable
|
||||
assert!(
|
||||
html.contains("<b>Start:</b> 20250911"),
|
||||
html.contains("<b>Start:</b> Thu Sep 11, 2025") || html.contains("<b>Start:</b> 20250911"),
|
||||
"HTML should contain the labeled start time"
|
||||
);
|
||||
assert!(
|
||||
html.contains("<b>End:</b> 20260131"),
|
||||
html.contains("<b>End:</b> Fri Jan 30, 2026") || html.contains("<b>End:</b> 20260131"),
|
||||
"HTML should contain the labeled end time"
|
||||
);
|
||||
if !html.contains("ical-flex") {
|
||||
@@ -2468,6 +2583,29 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn google_calendar_email_4_single_allday_event() {
|
||||
use mailparse::parse_mail;
|
||||
let raw_email = include_str!("../../server/testdata/google-calendar-example-4.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 for single all-day event
|
||||
assert_eq!(meta.summary, Some("Emery Sleeps Over".to_string()));
|
||||
assert_eq!(meta.organizer, Some("tconvertino@gmail.com".to_string()));
|
||||
// Dates: Sunday Jan 18, 2026 (all-day event)
|
||||
assert_eq!(meta.start_date, Some("20260118".to_string()));
|
||||
assert_eq!(meta.end_date, Some("20260119".to_string())); // All-day events end next day
|
||||
// Assert ical summary is rendered and shows Jan 18 highlighted
|
||||
let html = meta.body_html.expect("body_html");
|
||||
println!("Rendered HTML: {}", html);
|
||||
assert!(html.contains("ical-flex"), "Calendar widget should be rendered");
|
||||
assert!(html.contains(r#"data-event-day="2026-01-18""#), "Jan 18 should be highlighted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recurring_event_rrule_metadata_and_highlight() {
|
||||
use super::render_ical_summary;
|
||||
|
||||
Reference in New Issue
Block a user