server: most test to end of file
Some checks failed
Continuous integration / Check (push) Has been cancelled
Continuous integration / Test Suite (push) Has been cancelled
Continuous integration / Trunk (push) Has been cancelled
Continuous integration / Rustfmt (push) Has been cancelled
Continuous integration / build (push) Has been cancelled
Continuous integration / Disallow unused dependencies (push) Has been cancelled
Some checks failed
Continuous integration / Check (push) Has been cancelled
Continuous integration / Test Suite (push) Has been cancelled
Continuous integration / Trunk (push) Has been cancelled
Continuous integration / Rustfmt (push) Has been cancelled
Continuous integration / build (push) Has been cancelled
Continuous integration / Disallow unused dependencies (push) Has been cancelled
This commit is contained in:
parent
f3c5b4eb8c
commit
06e65a52b3
@ -1,3 +1,13 @@
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ExtractedCalendarMetadata {
|
||||
pub is_google_calendar_event: bool,
|
||||
pub summary: Option<String>,
|
||||
pub organizer: Option<String>,
|
||||
pub start_date: Option<String>,
|
||||
pub end_date: Option<String>,
|
||||
pub body_html: Option<String>,
|
||||
}
|
||||
|
||||
/// Helper to extract Google Calendar event metadata from a ParsedMail (for tests and features)
|
||||
pub fn extract_calendar_metadata_from_mail(
|
||||
m: &ParsedMail,
|
||||
@ -52,7 +62,7 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
summary = prop.value.clone();
|
||||
break 'event_loop;
|
||||
}
|
||||
},
|
||||
}
|
||||
"ORGANIZER" => organizer = prop.value.clone(),
|
||||
"DTSTART" => {
|
||||
if let Some(dt) = &prop.value {
|
||||
@ -82,18 +92,24 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
// Try to extract summary from subject (e.g., "New event: <summary> @ ...")
|
||||
if summary.is_none() {
|
||||
if let Some(subject) = m.headers.get_first_value("Subject") {
|
||||
if let Some(caps) = regex::Regex::new(r"New event: ([^@]+) @").ok().and_then(|re| re.captures(&subject)) {
|
||||
if let Some(caps) = regex::Regex::new(r"New event: ([^@]+) @")
|
||||
.ok()
|
||||
.and_then(|re| re.captures(&subject))
|
||||
{
|
||||
summary = Some(caps[1].trim().to_string());
|
||||
} else if let Some(caps) = regex::Regex::new(r"Invitation: ([^@]+) @").ok().and_then(|re| re.captures(&subject)) {
|
||||
} else if let Some(caps) = regex::Regex::new(r"Invitation: ([^@]+) @")
|
||||
.ok()
|
||||
.and_then(|re| re.captures(&subject))
|
||||
{
|
||||
summary = Some(caps[1].trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try to extract start/end dates from subject
|
||||
if start_date.is_none() || end_date.is_none() {
|
||||
if let Some(subject) = m.headers.get_first_value("Subject") {
|
||||
// Pattern: New event: Dentist appt @ Tue Sep 23, 2025 3pm - 4pm (PDT) (tconvertino@gmail.com)
|
||||
if let Some(caps) = regex::Regex::new(r"New event: [^@]+@ ([A-Za-z]{3}) ([A-Za-z]{3}) (\d{1,2}), (\d{4}) (\d{1,2})(?::(\d{2}))? ?([ap]m) ?- ?(\d{1,2})(?::(\d{2}))? ?([ap]m)").ok().and_then(|re| re.captures(&subject)) {
|
||||
if start_date.is_none() || end_date.is_none() {
|
||||
if let Some(subject) = m.headers.get_first_value("Subject") {
|
||||
// Pattern: New event: Dentist appt @ Tue Sep 23, 2025 3pm - 4pm (PDT) (tconvertino@gmail.com)
|
||||
if let Some(caps) = regex::Regex::new(r"New event: [^@]+@ ([A-Za-z]{3}) ([A-Za-z]{3}) (\d{1,2}), (\d{4}) (\d{1,2})(?::(\d{2}))? ?([ap]m) ?- ?(\d{1,2})(?::(\d{2}))? ?([ap]m)").ok().and_then(|re| re.captures(&subject)) {
|
||||
let month = &caps[2];
|
||||
let day = &caps[3];
|
||||
let year = &caps[4];
|
||||
@ -158,8 +174,8 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try to extract summary from body if still missing
|
||||
if summary.is_none() {
|
||||
if let Body::PlainText(t) = body {
|
||||
@ -173,7 +189,9 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
}
|
||||
if summary.is_none() {
|
||||
if let Body::Html(h) = body {
|
||||
let text = regex::Regex::new(r"<[^>]+>").unwrap().replace_all(&h.html, "");
|
||||
let text = regex::Regex::new(r"<[^>]+>")
|
||||
.unwrap()
|
||||
.replace_all(&h.html, "");
|
||||
for line in text.lines() {
|
||||
let line = line.trim();
|
||||
if !line.is_empty() && line.len() > 3 && line.len() < 100 {
|
||||
@ -205,7 +223,17 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
// Improved recurrence detection: check for common recurrence phrases in subject, HTML, and plain text body
|
||||
let mut has_recurrence = false;
|
||||
let recurrence_phrases = [
|
||||
"recurr", "repeat", "every week", "every month", "every year", "weekly", "monthly", "annually", "biweekly", "daily", "RRULE"
|
||||
"recurr",
|
||||
"repeat",
|
||||
"every week",
|
||||
"every month",
|
||||
"every year",
|
||||
"weekly",
|
||||
"monthly",
|
||||
"annually",
|
||||
"biweekly",
|
||||
"daily",
|
||||
"RRULE",
|
||||
];
|
||||
if let Some(ref s) = m.headers.get_first_value("Subject") {
|
||||
let subj = s.to_lowercase();
|
||||
@ -229,13 +257,18 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
}
|
||||
}
|
||||
}
|
||||
let needs_ical_flex = summary.is_some() || start_date.is_some() || end_date.is_some() || has_recurrence;
|
||||
let needs_ical_flex =
|
||||
summary.is_some() || start_date.is_some() || end_date.is_some() || has_recurrence;
|
||||
if needs_ical_flex {
|
||||
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();
|
||||
let end_val = end_date.clone().unwrap_or_default();
|
||||
let recurrence_display = if has_recurrence { "Repeats".to_string() } else { String::new() };
|
||||
let recurrence_display = if has_recurrence {
|
||||
"Repeats".to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let template = IcalSummaryTemplate {
|
||||
summary: &summary_val,
|
||||
local_fmt_start: &start_val,
|
||||
@ -249,13 +282,15 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
today: Some(chrono::Local::now().date_naive()),
|
||||
recurrence_display,
|
||||
};
|
||||
let fallback_html = template.render().unwrap_or_else(|_| String::from("<div class='ical-flex'></div>"));
|
||||
let fallback_html = template
|
||||
.render()
|
||||
.unwrap_or_else(|_| String::from("<div class='ical-flex'></div>"));
|
||||
match &mut body_html {
|
||||
Some(existing) => {
|
||||
if !existing.starts_with(&fallback_html) {
|
||||
*existing = format!("{}{}", fallback_html, existing);
|
||||
}
|
||||
},
|
||||
}
|
||||
None => {
|
||||
body_html = Some(fallback_html);
|
||||
}
|
||||
@ -276,16 +311,26 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
today: Some(chrono::Local::now().date_naive()),
|
||||
recurrence_display: String::new(),
|
||||
};
|
||||
body_html = Some(template.render().unwrap_or_else(|_| String::from("<div class='ical-flex'></div>")));
|
||||
body_html = Some(
|
||||
template
|
||||
.render()
|
||||
.unwrap_or_else(|_| String::from("<div class='ical-flex'></div>")),
|
||||
);
|
||||
}
|
||||
|
||||
// Improved fallback: extract summary, start_date, end_date, and recurrence from subject/body if not found
|
||||
if let Some(subject) = m.headers.get_first_value("Subject") {
|
||||
// Try to extract summary from subject (e.g., "New event: <summary> @ ...")
|
||||
if summary.is_none() {
|
||||
if let Some(caps) = regex::Regex::new(r"New event: ([^@]+) @").ok().and_then(|re| re.captures(&subject)) {
|
||||
if let Some(caps) = regex::Regex::new(r"New event: ([^@]+) @")
|
||||
.ok()
|
||||
.and_then(|re| re.captures(&subject))
|
||||
{
|
||||
summary = Some(caps[1].trim().to_string());
|
||||
} else if let Some(caps) = regex::Regex::new(r"Invitation: ([^@]+) @").ok().and_then(|re| re.captures(&subject)) {
|
||||
} else if let Some(caps) = regex::Regex::new(r"Invitation: ([^@]+) @")
|
||||
.ok()
|
||||
.and_then(|re| re.captures(&subject))
|
||||
{
|
||||
summary = Some(caps[1].trim().to_string());
|
||||
}
|
||||
}
|
||||
@ -346,7 +391,7 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
}
|
||||
}
|
||||
// Try to detect recurrence from subject
|
||||
// recurrence detection and rendering is now handled by the template logic
|
||||
// recurrence detection and rendering is now handled by the template logic
|
||||
}
|
||||
// Try to extract summary from body if still missing
|
||||
if summary.is_none() {
|
||||
@ -361,7 +406,9 @@ pub fn extract_calendar_metadata_from_mail(
|
||||
}
|
||||
if summary.is_none() {
|
||||
if let Body::Html(h) = body {
|
||||
let text = regex::Regex::new(r"<[^>]+>").unwrap().replace_all(&h.html, "");
|
||||
let text = regex::Regex::new(r"<[^>]+>")
|
||||
.unwrap()
|
||||
.replace_all(&h.html, "");
|
||||
for line in text.lines() {
|
||||
let line = line.trim();
|
||||
if !line.is_empty() && line.len() > 3 && line.len() < 100 {
|
||||
@ -2106,6 +2153,7 @@ fn parse_ical_datetime_tz(dt: &str, tz: Tz) -> Option<chrono::DateTime<Tz>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn google_calendar_email_3_single_event_metadata() {
|
||||
use mailparse::parse_mail;
|
||||
@ -2125,9 +2173,18 @@ mod tests {
|
||||
assert_eq!(meta.end_date, Some("20250923".to_string()));
|
||||
// Should not be recurring
|
||||
if let Some(ref html) = meta.body_html {
|
||||
assert!(html.contains("Dentist appt"), "HTML should contain the summary");
|
||||
assert!(html.contains("20250923"), "HTML should contain the event date");
|
||||
assert!(!html.contains("Repeats"), "HTML should not mention recurrence");
|
||||
assert!(
|
||||
html.contains("Dentist appt"),
|
||||
"HTML should contain the summary"
|
||||
);
|
||||
assert!(
|
||||
html.contains("20250923"),
|
||||
"HTML should contain the event date"
|
||||
);
|
||||
assert!(
|
||||
!html.contains("Repeats"),
|
||||
"HTML should not mention recurrence"
|
||||
);
|
||||
} else {
|
||||
panic!("No body_html rendered");
|
||||
}
|
||||
@ -2145,7 +2202,10 @@ mod tests {
|
||||
// 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()));
|
||||
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)));
|
||||
@ -2153,7 +2213,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn google_calendar_email_2_renders_calendar_and_recurrence() {
|
||||
// ...existing code...
|
||||
// ...existing code...
|
||||
use mailparse::parse_mail;
|
||||
let raw_email = include_str!("../../server/testdata/google-calendar-example-2.eml");
|
||||
let parsed = parse_mail(raw_email.as_bytes()).expect("parse_mail");
|
||||
@ -2164,25 +2224,47 @@ mod tests {
|
||||
let html = meta.body_html.expect("body_html");
|
||||
println!("Rendered HTML for verification:\n{}", html);
|
||||
// Check that the HTML contains the summary, organizer, start, and end times with labels
|
||||
assert!(html.contains("<b>Summary:</b> McClure BLT"), "HTML should contain the labeled summary/title");
|
||||
assert!(html.contains("<b>Organizer:</b> calendar-notification@google.com"), "HTML should contain the labeled organizer");
|
||||
assert!(html.contains("<b>Start:</b> 20250911"), "HTML should contain the labeled start time");
|
||||
assert!(html.contains("<b>End:</b> 20260131"), "HTML should contain the labeled end time");
|
||||
assert!(
|
||||
html.contains("<b>Summary:</b> McClure BLT"),
|
||||
"HTML should contain the labeled summary/title"
|
||||
);
|
||||
assert!(
|
||||
html.contains("<b>Organizer:</b> calendar-notification@google.com"),
|
||||
"HTML should contain the labeled organizer"
|
||||
);
|
||||
assert!(
|
||||
html.contains("<b>Start:</b> 20250911"),
|
||||
"HTML should contain the labeled start time"
|
||||
);
|
||||
assert!(
|
||||
html.contains("<b>End:</b> 20260131"),
|
||||
"HTML should contain the labeled end time"
|
||||
);
|
||||
if !html.contains("ical-flex") {
|
||||
println!("FAIL: html did not contain 'ical-flex':\n{}", html);
|
||||
}
|
||||
assert!(html.contains("ical-flex"), "Calendar widget should be rendered");
|
||||
assert!(
|
||||
html.contains("ical-flex"),
|
||||
"Calendar widget should be rendered"
|
||||
);
|
||||
// Recurrence info should be present
|
||||
if !(html.contains("<b>Repeats:</b> Repeats") || html.contains("recurr") || html.contains("RRULE")) {
|
||||
if !(html.contains("<b>Repeats:</b> Repeats")
|
||||
|| html.contains("recurr")
|
||||
|| html.contains("RRULE"))
|
||||
{
|
||||
println!("FAIL: html did not contain recurrence info:\n{}", html);
|
||||
}
|
||||
assert!(html.contains("<b>Repeats:</b> Repeats") || html.contains("recurr") || html.contains("RRULE"), "Recurrence info should be present in HTML");
|
||||
assert!(
|
||||
html.contains("<b>Repeats:</b> Repeats")
|
||||
|| html.contains("recurr")
|
||||
|| html.contains("RRULE"),
|
||||
"Recurrence info should be present in HTML"
|
||||
);
|
||||
}
|
||||
use super::*;
|
||||
#[test]
|
||||
fn google_calendar_email_renders_ical_summary() {
|
||||
use mailparse::parse_mail;
|
||||
let raw_email = include_str!("../../server/testdata/google-calendar-example.eml");
|
||||
let raw_email = include_str!("../testdata/google-calendar-example.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");
|
||||
@ -2255,7 +2337,10 @@ mod tests {
|
||||
assert!(meta.is_google_calendar_event);
|
||||
// Assert that the summary and organizer are present
|
||||
assert_eq!(meta.summary, Some("McClure BLT".to_string()));
|
||||
assert_eq!(meta.organizer, Some("calendar-notification@google.com".to_string()));
|
||||
assert_eq!(
|
||||
meta.organizer,
|
||||
Some("calendar-notification@google.com".to_string())
|
||||
);
|
||||
// Assert that the start and end dates are present
|
||||
let current_year = chrono::Local::now().year();
|
||||
assert_eq!(meta.start_date, Some(format!("{}0911", current_year)));
|
||||
@ -2265,7 +2350,10 @@ mod tests {
|
||||
if !(html.contains("Repeats") || html.contains("recurr") || html.contains("RRULE")) {
|
||||
println!("FAIL: html did not contain recurrence info:\n{}", html);
|
||||
}
|
||||
assert!(html.contains("Repeats") || html.contains("recurr") || html.contains("RRULE"), "Recurrence info should be present in HTML");
|
||||
assert!(
|
||||
html.contains("Repeats") || html.contains("recurr") || html.contains("RRULE"),
|
||||
"Recurrence info should be present in HTML"
|
||||
);
|
||||
} else {
|
||||
panic!("No body_html rendered");
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user