diff --git a/backend/src/calendar.rs b/backend/src/calendar.rs index 5702a85..00153ab 100644 --- a/backend/src/calendar.rs +++ b/backend/src/calendar.rs @@ -330,13 +330,26 @@ impl CalDAVClient { event: ical::parser::ical::component::IcalEvent, ) -> Result { let mut properties: HashMap = HashMap::new(); + let mut full_properties: HashMap = 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, 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::() { + // 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 ))) }