Compare commits
4 Commits
letterbox-
...
letterbox-
| Author | SHA1 | Date | |
|---|---|---|---|
| 17cdae7bfb | |||
| f89135fce5 | |||
| 38c1d140bd | |||
| 197ea049b2 |
44
Cargo.lock
generated
44
Cargo.lock
generated
@@ -222,9 +222,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-graphql"
|
name = "async-graphql"
|
||||||
version = "7.2.0"
|
version = "7.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57b75e6d81f69e47038fb2f08c54dc9180fabef56856b7a74e4082157f2e5536"
|
checksum = "1057a9f7ccf2404d94571dec3451ade1cb524790df6f1ada0d19c2a49f6b0f40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-graphql-derive",
|
"async-graphql-derive",
|
||||||
"async-graphql-parser",
|
"async-graphql-parser",
|
||||||
@@ -257,9 +257,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-graphql-axum"
|
name = "async-graphql-axum"
|
||||||
version = "7.2.0"
|
version = "7.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6197f3d8bc7dae675a5d82ce45316802a0569801ff5ce9cda6d0514cb80bee57"
|
checksum = "a1e37c5532e4b686acf45e7162bc93da91fc2c702fb0d465efc2c20c8f973795"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-graphql",
|
"async-graphql",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -274,9 +274,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-graphql-derive"
|
name = "async-graphql-derive"
|
||||||
version = "7.2.0"
|
version = "7.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8587c1c72749f54250633a725203d537ebda851b68d85c2a8d18a3adc0bf72d6"
|
checksum = "2e6cbeadc8515e66450fba0985ce722192e28443697799988265d86304d7cc68"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Inflector",
|
"Inflector",
|
||||||
"async-graphql-parser",
|
"async-graphql-parser",
|
||||||
@@ -291,9 +291,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-graphql-parser"
|
name = "async-graphql-parser"
|
||||||
version = "7.2.0"
|
version = "7.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "577ec8cb624048d11465439c2b25d28362cb08c154b530421f456debc7083fdf"
|
checksum = "e64ef70f77a1c689111e52076da1cd18f91834bcb847de0a9171f83624b07fbf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-graphql-value",
|
"async-graphql-value",
|
||||||
"pest",
|
"pest",
|
||||||
@@ -303,9 +303,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-graphql-value"
|
name = "async-graphql-value"
|
||||||
version = "7.2.0"
|
version = "7.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e747684314ff7454a1f3b6fe5341e15148b1f17f30c9f6ecc55832dd1f053c47"
|
checksum = "3e3ef112905abea9dea592fc868a6873b10ebd3f983e83308f995d6284e9ba41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 1.11.0",
|
"bytes 1.11.0",
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
@@ -2807,7 +2807,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.5.10",
|
"socket2 0.6.1",
|
||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio 1.49.0",
|
"tokio 1.49.0",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@@ -3179,7 +3179,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "letterbox-notmuch"
|
name = "letterbox-notmuch"
|
||||||
version = "0.17.60"
|
version = "0.17.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itertools",
|
"itertools",
|
||||||
"log",
|
"log",
|
||||||
@@ -3194,7 +3194,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "letterbox-procmail2notmuch"
|
name = "letterbox-procmail2notmuch"
|
||||||
version = "0.17.60"
|
version = "0.17.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -3207,7 +3207,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "letterbox-server"
|
name = "letterbox-server"
|
||||||
version = "0.17.60"
|
version = "0.17.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -3230,8 +3230,8 @@ dependencies = [
|
|||||||
"html-escape",
|
"html-escape",
|
||||||
"html2text",
|
"html2text",
|
||||||
"ical",
|
"ical",
|
||||||
"letterbox-notmuch 0.17.60",
|
"letterbox-notmuch 0.17.61",
|
||||||
"letterbox-shared 0.17.60",
|
"letterbox-shared 0.17.61",
|
||||||
"linkify",
|
"linkify",
|
||||||
"lol_html",
|
"lol_html",
|
||||||
"mailparse",
|
"mailparse",
|
||||||
@@ -3272,10 +3272,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "letterbox-shared"
|
name = "letterbox-shared"
|
||||||
version = "0.17.60"
|
version = "0.17.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"build-info",
|
"build-info",
|
||||||
"letterbox-notmuch 0.17.60",
|
"letterbox-notmuch 0.17.61",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
@@ -3285,7 +3285,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "letterbox-web"
|
name = "letterbox-web"
|
||||||
version = "0.17.60"
|
version = "0.17.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"build-info",
|
"build-info",
|
||||||
"build-info-build",
|
"build-info-build",
|
||||||
@@ -3297,7 +3297,7 @@ dependencies = [
|
|||||||
"graphql_client",
|
"graphql_client",
|
||||||
"human_format",
|
"human_format",
|
||||||
"itertools",
|
"itertools",
|
||||||
"letterbox-shared 0.17.60",
|
"letterbox-shared 0.17.61",
|
||||||
"log",
|
"log",
|
||||||
"seed",
|
"seed",
|
||||||
"seed_hooks",
|
"seed_hooks",
|
||||||
@@ -4622,7 +4622,7 @@ dependencies = [
|
|||||||
"quinn-udp",
|
"quinn-udp",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2 0.5.10",
|
"socket2 0.6.1",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio 1.49.0",
|
"tokio 1.49.0",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -4660,7 +4660,7 @@ dependencies = [
|
|||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2 0.5.10",
|
"socket2 0.6.1",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ authors = ["Bill Thiede <git@xinu.tv>"]
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "UNLICENSED"
|
license = "UNLICENSED"
|
||||||
publish = ["xinu"]
|
publish = ["xinu"]
|
||||||
version = "0.17.60"
|
version = "0.17.61"
|
||||||
repository = "https://git.z.xinu.tv/wathiede/letterbox"
|
repository = "https://git.z.xinu.tv/wathiede/letterbox"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
|
|||||||
@@ -178,11 +178,36 @@ pub fn extract_calendar_metadata_from_mail(
|
|||||||
let month = &caps[2];
|
let month = &caps[2];
|
||||||
let day = &caps[3];
|
let day = &caps[3];
|
||||||
let year = &caps[4];
|
let year = &caps[4];
|
||||||
|
let start_hour: u32 = caps[5].parse().unwrap_or(0);
|
||||||
|
let start_min: u32 = caps.get(6).map(|m| m.as_str().parse().unwrap_or(0)).unwrap_or(0);
|
||||||
|
let start_ampm = &caps[7];
|
||||||
|
let end_hour: u32 = caps[8].parse().unwrap_or(0);
|
||||||
|
let end_min: u32 = caps.get(9).map(|m| m.as_str().parse().unwrap_or(0)).unwrap_or(0);
|
||||||
|
let end_ampm = &caps[10];
|
||||||
|
|
||||||
|
// Convert 12-hour to 24-hour format
|
||||||
|
let start_hour_24 = if start_ampm == "pm" && start_hour != 12 {
|
||||||
|
start_hour + 12
|
||||||
|
} else if start_ampm == "am" && start_hour == 12 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
start_hour
|
||||||
|
};
|
||||||
|
let end_hour_24 = if end_ampm == "pm" && end_hour != 12 {
|
||||||
|
end_hour + 12
|
||||||
|
} else if end_ampm == "am" && end_hour == 12 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
end_hour
|
||||||
|
};
|
||||||
|
|
||||||
let date_str = format!("{} {} {}", month, day, year);
|
let date_str = format!("{} {} {}", month, day, year);
|
||||||
if let Ok(date) = chrono::NaiveDate::parse_from_str(&date_str, "%b %d %Y") {
|
if let Ok(date) = chrono::NaiveDate::parse_from_str(&date_str, "%b %d %Y") {
|
||||||
let ymd = date.format("%Y%m%d").to_string();
|
// Store date with time in YYYYMMDDTHHMMSS format for start/end
|
||||||
start_date = Some(ymd.clone());
|
let start_dt = format!("{}T{:02}{:02}00", date.format("%Y%m%d"), start_hour_24, start_min);
|
||||||
end_date = Some(ymd);
|
let end_dt = format!("{}T{:02}{:02}00", date.format("%Y%m%d"), end_hour_24, end_min);
|
||||||
|
start_date = Some(start_dt);
|
||||||
|
end_date = Some(end_dt);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Pattern: from Thu Sep 11 to Fri Jan 30, 2026
|
// Pattern: from Thu Sep 11 to Fri Jan 30, 2026
|
||||||
@@ -375,13 +400,37 @@ pub fn extract_calendar_metadata_from_mail(
|
|||||||
let (local_fmt_start, local_fmt_end) = if let (Some(ref start_str), Some(ref end_str)) =
|
let (local_fmt_start, local_fmt_end) = if let (Some(ref start_str), Some(ref end_str)) =
|
||||||
(&start_date, &end_date)
|
(&start_date, &end_date)
|
||||||
{
|
{
|
||||||
// Parse YYYYMMDD format dates
|
// Parse dates - try YYYYMMDDTHHMMSS format first (with time), then YYYYMMDD (date only)
|
||||||
let start_d = NaiveDate::parse_from_str(start_str, "%Y%m%d").ok();
|
let (start_d, start_time) = if start_str.contains('T') {
|
||||||
let end_d = NaiveDate::parse_from_str(end_str, "%Y%m%d").ok();
|
let parts: Vec<&str> = start_str.split('T').collect();
|
||||||
|
let date = NaiveDate::parse_from_str(parts[0], "%Y%m%d").ok();
|
||||||
|
let time = if parts.len() > 1 {
|
||||||
|
chrono::NaiveTime::parse_from_str(parts[1], "%H%M%S").ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
(date, time)
|
||||||
|
} else {
|
||||||
|
(NaiveDate::parse_from_str(start_str, "%Y%m%d").ok(), None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (end_d, end_time) = if end_str.contains('T') {
|
||||||
|
let parts: Vec<&str> = end_str.split('T').collect();
|
||||||
|
let date = NaiveDate::parse_from_str(parts[0], "%Y%m%d").ok();
|
||||||
|
let time = if parts.len() > 1 {
|
||||||
|
chrono::NaiveTime::parse_from_str(parts[1], "%H%M%S").ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
(date, time)
|
||||||
|
} else {
|
||||||
|
(NaiveDate::parse_from_str(end_str, "%Y%m%d").ok(), None)
|
||||||
|
};
|
||||||
|
|
||||||
if let (Some(start), Some(end)) = (start_d, end_d) {
|
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
|
// For all-day events (no time), end date is exclusive, so we need to subtract one day
|
||||||
let display_end = if end > start {
|
let is_allday = start_time.is_none() && end_time.is_none();
|
||||||
|
let display_end = if is_allday && end > start {
|
||||||
end.pred_opt().unwrap_or(end)
|
end.pred_opt().unwrap_or(end)
|
||||||
} else {
|
} else {
|
||||||
end
|
end
|
||||||
@@ -398,9 +447,17 @@ pub fn extract_calendar_metadata_from_mail(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format dates for display
|
// Format dates for display - include time if available
|
||||||
let fmt_start = start.format("%a %b %e, %Y").to_string();
|
let fmt_start = if let Some(t) = start_time {
|
||||||
let fmt_end = display_end.format("%a %b %e, %Y").to_string();
|
format!("{} {}", start.format("%a %b %e, %Y"), t.format("%-I:%M %p"))
|
||||||
|
} else {
|
||||||
|
start.format("%a %b %e, %Y").to_string()
|
||||||
|
};
|
||||||
|
let fmt_end = if let Some(t) = end_time {
|
||||||
|
format!("{} {}", display_end.format("%a %b %e, %Y"), t.format("%-I:%M %p"))
|
||||||
|
} else {
|
||||||
|
display_end.format("%a %b %e, %Y").to_string()
|
||||||
|
};
|
||||||
(fmt_start, fmt_end)
|
(fmt_start, fmt_end)
|
||||||
} else {
|
} else {
|
||||||
(start_val.clone(), end_val.clone())
|
(start_val.clone(), end_val.clone())
|
||||||
@@ -2395,9 +2452,9 @@ mod tests {
|
|||||||
assert_eq!(meta.summary, Some("Dentist appt".to_string()));
|
assert_eq!(meta.summary, Some("Dentist appt".to_string()));
|
||||||
// Organizer: from From header, extract email address
|
// Organizer: from From header, extract email address
|
||||||
assert_eq!(meta.organizer, Some("tconvertino@gmail.com".to_string()));
|
assert_eq!(meta.organizer, Some("tconvertino@gmail.com".to_string()));
|
||||||
// Dates: should extract Sep 23, 2025, 3pm-4pm
|
// Dates: should extract Sep 23, 2025, 3pm-4pm (15:00-16:00)
|
||||||
assert_eq!(meta.start_date, Some("20250923".to_string()));
|
assert_eq!(meta.start_date, Some("20250923T150000".to_string()));
|
||||||
assert_eq!(meta.end_date, Some("20250923".to_string()));
|
assert_eq!(meta.end_date, Some("20250923T160000".to_string()));
|
||||||
// Should not be recurring
|
// Should not be recurring
|
||||||
if let Some(ref html) = meta.body_html {
|
if let Some(ref html) = meta.body_html {
|
||||||
assert!(
|
assert!(
|
||||||
@@ -2625,13 +2682,17 @@ mod tests {
|
|||||||
assert_eq!(meta.summary, Some("painting class".to_string()));
|
assert_eq!(meta.summary, Some("painting class".to_string()));
|
||||||
assert_eq!(meta.organizer, Some("tconvertino@gmail.com".to_string()));
|
assert_eq!(meta.organizer, Some("tconvertino@gmail.com".to_string()));
|
||||||
// Dates: Thursday Feb 12, 2026 7pm - 9pm (same day event with time)
|
// Dates: Thursday Feb 12, 2026 7pm - 9pm (same day event with time)
|
||||||
assert_eq!(meta.start_date, Some("20260212".to_string()));
|
// Start: 7pm = 19:00, End: 9pm = 21:00
|
||||||
assert_eq!(meta.end_date, Some("20260212".to_string()));
|
assert_eq!(meta.start_date, Some("20260212T190000".to_string()));
|
||||||
|
assert_eq!(meta.end_date, Some("20260212T210000".to_string()));
|
||||||
// Assert ical summary is rendered and shows Feb 12 highlighted
|
// Assert ical summary is rendered and shows Feb 12 highlighted
|
||||||
let html = meta.body_html.expect("body_html");
|
let html = meta.body_html.expect("body_html");
|
||||||
println!("Rendered HTML: {}", html);
|
println!("Rendered HTML: {}", html);
|
||||||
assert!(html.contains("ical-flex"), "Calendar widget should be rendered");
|
assert!(html.contains("ical-flex"), "Calendar widget should be rendered");
|
||||||
assert!(html.contains(r#"data-event-day="2026-02-12""#), "Feb 12 should be highlighted");
|
assert!(html.contains(r#"data-event-day="2026-02-12""#), "Feb 12 should be highlighted");
|
||||||
|
// Verify time is displayed in the HTML
|
||||||
|
assert!(html.contains("7:00 PM") || html.contains("7pm") || html.contains("19:00"),
|
||||||
|
"HTML should contain the start time");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user