Fix CalDAV timezone parsing for external client events
Events created in external CalDAV clients (like AgendaV) with timezone information were showing incorrect times due to improper timezone handling. Fixed by: - Enhanced datetime parser to extract TZID parameters from iCal properties - Added proper timezone conversion from source timezone to UTC using chrono-tz - Preserved full property strings with parameters during parsing - Maintained backward compatibility with existing UTC format events This resolves the issue where events created at 9 AM Mountain Time were displaying as 5 AM instead of the correct 11 AM Eastern Time. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -330,13 +330,26 @@ impl CalDAVClient {
|
|||||||
event: ical::parser::ical::component::IcalEvent,
|
event: ical::parser::ical::component::IcalEvent,
|
||||||
) -> Result<CalendarEvent, CalDAVError> {
|
) -> Result<CalendarEvent, CalDAVError> {
|
||||||
let mut properties: HashMap<String, String> = HashMap::new();
|
let mut properties: HashMap<String, String> = HashMap::new();
|
||||||
|
let mut full_properties: HashMap<String, String> = HashMap::new();
|
||||||
|
|
||||||
// Extract all properties from the event
|
// Extract all properties from the event
|
||||||
for property in &event.properties {
|
for property in &event.properties {
|
||||||
properties.insert(
|
let prop_name = property.name.to_uppercase();
|
||||||
property.name.to_uppercase(),
|
let prop_value = property.value.clone().unwrap_or_default();
|
||||||
property.value.clone().unwrap_or_default(),
|
|
||||||
);
|
properties.insert(prop_name.clone(), prop_value.clone());
|
||||||
|
|
||||||
|
// Build full property string with parameters for timezone parsing
|
||||||
|
let mut full_prop = format!("{}", prop_name);
|
||||||
|
if let Some(params) = &property.params {
|
||||||
|
for (param_name, param_values) in params {
|
||||||
|
if !param_values.is_empty() {
|
||||||
|
full_prop.push_str(&format!(";{}={}", param_name, param_values.join(",")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
full_prop.push_str(&format!(":{}", prop_value));
|
||||||
|
full_properties.insert(prop_name, full_prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required UID field
|
// Required UID field
|
||||||
@@ -349,11 +362,11 @@ impl CalDAVClient {
|
|||||||
let start = properties
|
let start = properties
|
||||||
.get("DTSTART")
|
.get("DTSTART")
|
||||||
.ok_or_else(|| CalDAVError::ParseError("Missing DTSTART field".to_string()))?;
|
.ok_or_else(|| CalDAVError::ParseError("Missing DTSTART field".to_string()))?;
|
||||||
let start = self.parse_datetime(start, properties.get("DTSTART"))?;
|
let start = self.parse_datetime(start, full_properties.get("DTSTART"))?;
|
||||||
|
|
||||||
// Parse end time (optional - use start time if not present)
|
// Parse end time (optional - use start time if not present)
|
||||||
let end = if let Some(dtend) = properties.get("DTEND") {
|
let end = if let Some(dtend) = properties.get("DTEND") {
|
||||||
Some(self.parse_datetime(dtend, properties.get("DTEND"))?)
|
Some(self.parse_datetime(dtend, full_properties.get("DTEND"))?)
|
||||||
} else if let Some(_duration) = properties.get("DURATION") {
|
} else if let Some(_duration) = properties.get("DURATION") {
|
||||||
// TODO: Parse duration and add to start time
|
// TODO: Parse duration and add to start time
|
||||||
Some(start)
|
Some(start)
|
||||||
@@ -671,16 +684,39 @@ impl CalDAVClient {
|
|||||||
Ok(calendar_paths)
|
Ok(calendar_paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse iCal datetime format
|
/// Parse iCal datetime format with timezone support
|
||||||
fn parse_datetime(
|
fn parse_datetime(
|
||||||
&self,
|
&self,
|
||||||
datetime_str: &str,
|
datetime_str: &str,
|
||||||
_original_property: Option<&String>,
|
original_property: Option<&String>,
|
||||||
) -> Result<DateTime<Utc>, CalDAVError> {
|
) -> Result<DateTime<Utc>, CalDAVError> {
|
||||||
use chrono::TimeZone;
|
use chrono::TimeZone;
|
||||||
|
use chrono_tz::Tz;
|
||||||
|
|
||||||
// Handle different iCal datetime formats
|
// Extract timezone information from the original property if available
|
||||||
|
let mut timezone_id: Option<&str> = None;
|
||||||
|
if let Some(prop) = original_property {
|
||||||
|
// Look for TZID parameter in the property
|
||||||
|
// Format: DTSTART;TZID=America/Denver:20231225T090000
|
||||||
|
if let Some(tzid_start) = prop.find("TZID=") {
|
||||||
|
let tzid_part = &prop[tzid_start + 5..];
|
||||||
|
if let Some(tzid_end) = tzid_part.find(':') {
|
||||||
|
timezone_id = Some(&tzid_part[..tzid_end]);
|
||||||
|
} else if let Some(tzid_end) = tzid_part.find(';') {
|
||||||
|
timezone_id = Some(&tzid_part[..tzid_end]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean the datetime string - remove any TZID prefix if present
|
||||||
let cleaned = datetime_str.replace("TZID=", "").trim().to_string();
|
let cleaned = datetime_str.replace("TZID=", "").trim().to_string();
|
||||||
|
|
||||||
|
// Split on colon to separate TZID from datetime if format is "TZID=America/Denver:20231225T090000"
|
||||||
|
let datetime_part = if let Some(colon_pos) = cleaned.find(':') {
|
||||||
|
&cleaned[colon_pos + 1..]
|
||||||
|
} else {
|
||||||
|
&cleaned
|
||||||
|
};
|
||||||
|
|
||||||
// Try different parsing formats
|
// Try different parsing formats
|
||||||
let formats = [
|
let formats = [
|
||||||
@@ -690,17 +726,39 @@ impl CalDAVClient {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for format in &formats {
|
for format in &formats {
|
||||||
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&cleaned, format) {
|
// Try parsing as UTC first (if it has Z suffix)
|
||||||
return Ok(Utc.from_utc_datetime(&dt));
|
if datetime_part.ends_with('Z') {
|
||||||
|
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&datetime_part[..datetime_part.len()-1], "%Y%m%dT%H%M%S") {
|
||||||
|
return Ok(dt.and_utc());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Ok(date) = chrono::NaiveDate::parse_from_str(&cleaned, format) {
|
|
||||||
|
// Try parsing as naive datetime
|
||||||
|
if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(datetime_part, format) {
|
||||||
|
// If we have timezone information, convert accordingly
|
||||||
|
if let Some(tz_id) = timezone_id {
|
||||||
|
if let Ok(tz) = tz_id.parse::<Tz>() {
|
||||||
|
// Convert from the specified timezone to UTC
|
||||||
|
if let Some(local_dt) = tz.from_local_datetime(&naive_dt).single() {
|
||||||
|
let utc_dt = local_dt.with_timezone(&Utc);
|
||||||
|
return Ok(utc_dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If timezone parsing fails, log and fall back to UTC
|
||||||
|
}
|
||||||
|
// No timezone info or parsing failed - treat as UTC
|
||||||
|
return Ok(Utc.from_utc_datetime(&naive_dt));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing as date only
|
||||||
|
if let Ok(date) = chrono::NaiveDate::parse_from_str(datetime_part, format) {
|
||||||
return Ok(Utc.from_utc_datetime(&date.and_hms_opt(0, 0, 0).unwrap()));
|
return Ok(Utc.from_utc_datetime(&date.and_hms_opt(0, 0, 0).unwrap()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(CalDAVError::ParseError(format!(
|
Err(CalDAVError::ParseError(format!(
|
||||||
"Unable to parse datetime: {}",
|
"Unable to parse datetime: {} (cleaned: {}, timezone: {:?})",
|
||||||
datetime_str
|
datetime_str, datetime_part, timezone_id
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user