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,
|
||||
) -> Result<CalendarEvent, CalDAVError> {
|
||||
let mut properties: HashMap<String, String> = HashMap::new();
|
||||
let mut full_properties: HashMap<String, String> = HashMap::new();
|
||||
|
||||
// Extract all properties from the event
|
||||
for property in &event.properties {
|
||||
properties.insert(
|
||||
property.name.to_uppercase(),
|
||||
property.value.clone().unwrap_or_default(),
|
||||
);
|
||||
let prop_name = property.name.to_uppercase();
|
||||
let prop_value = 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
|
||||
@@ -349,11 +362,11 @@ impl CalDAVClient {
|
||||
let start = properties
|
||||
.get("DTSTART")
|
||||
.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)
|
||||
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") {
|
||||
// TODO: Parse duration and add to start time
|
||||
Some(start)
|
||||
@@ -671,16 +684,39 @@ impl CalDAVClient {
|
||||
Ok(calendar_paths)
|
||||
}
|
||||
|
||||
/// Parse iCal datetime format
|
||||
/// Parse iCal datetime format with timezone support
|
||||
fn parse_datetime(
|
||||
&self,
|
||||
datetime_str: &str,
|
||||
_original_property: Option<&String>,
|
||||
original_property: Option<&String>,
|
||||
) -> Result<DateTime<Utc>, CalDAVError> {
|
||||
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();
|
||||
|
||||
// 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
|
||||
let formats = [
|
||||
@@ -690,17 +726,39 @@ impl CalDAVClient {
|
||||
];
|
||||
|
||||
for format in &formats {
|
||||
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&cleaned, format) {
|
||||
return Ok(Utc.from_utc_datetime(&dt));
|
||||
// Try parsing as UTC first (if it has Z suffix)
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
Err(CalDAVError::ParseError(format!(
|
||||
"Unable to parse datetime: {}",
|
||||
datetime_str
|
||||
"Unable to parse datetime: {} (cleaned: {}, timezone: {:?})",
|
||||
datetime_str, datetime_part, timezone_id
|
||||
)))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user