diff --git a/Cargo.lock b/Cargo.lock index ddf7288..e377af2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3282,6 +3282,7 @@ dependencies = [ "tower", "tower-sessions", "tracing", + "vtimezones-rs", ] [[package]] diff --git a/crates/caldav/src/calendar/methods/mkcalendar.rs b/crates/caldav/src/calendar/methods/mkcalendar.rs index 4787cab..95ff9d6 100644 --- a/crates/caldav/src/calendar/methods/mkcalendar.rs +++ b/crates/caldav/src/calendar/methods/mkcalendar.rs @@ -4,6 +4,7 @@ use crate::calendar::prop::SupportedCalendarComponentSet; use axum::extract::{Path, State}; use axum::response::{IntoResponse, Response}; use http::{Method, StatusCode}; +use ical::IcalParser; use rustical_dav::xml::HrefElement; use rustical_ical::CalendarObjectType; use rustical_store::auth::Principal; @@ -82,26 +83,38 @@ pub async fn route_mkcalendar( request.displayname = None } - let mut timezone = request.calendar_timezone; - if let Some(tzid) = request.calendar_timezone_id.as_ref() { - // Validate timezone id and set timezone accordingly - timezone = Some( - vtimezones_rs::VTIMEZONES - .get(tzid) - .ok_or(rustical_dav::Error::BadRequest(format!( - "Invalid timezone-id: {tzid}" - )))? - .to_string(), - ); - } + let timezone_id = if let Some(tzid) = request.calendar_timezone_id { + Some(tzid) + } else if let Some(tz) = request.calendar_timezone { + // TODO: Proper error (calendar-timezone precondition) + let calendar = IcalParser::new(tz.as_bytes()) + .next() + .ok_or(rustical_dav::Error::BadRequest( + "No timezone data provided".to_owned(), + ))? + .map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?; + + let timezone = calendar + .timezones + .first() + .ok_or(rustical_dav::Error::BadRequest( + "No timezone data provided".to_owned(), + ))?; + let timezone: chrono_tz::Tz = timezone + .try_into() + .map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?; + + Some(timezone.name().to_owned()) + } else { + None + }; let calendar = Calendar { id: cal_id.to_owned(), principal: principal.to_owned(), order: request.calendar_order.unwrap_or(0), displayname: request.displayname, - timezone, - timezone_id: request.calendar_timezone_id, + timezone_id, color: request.calendar_color, description: request.calendar_description, deleted_at: None, diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index 6704afa..37b7c57 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -3,6 +3,7 @@ use crate::Error; use crate::calendar::prop::ReportMethod; use chrono::{DateTime, Utc}; use derive_more::derive::{From, Into}; +use ical::IcalParser; use rustical_dav::extensions::{ CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp, }; @@ -132,7 +133,9 @@ impl Resource for CalendarResource { CalendarProp::CalendarDescription(self.cal.description.clone()) } CalendarPropName::CalendarTimezone => { - CalendarProp::CalendarTimezone(self.cal.timezone.clone()) + CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| { + vtimezones_rs::VTIMEZONES.get(tzid).map(|tz| tz.to_string()) + })) } // chrono_tz uses the IANA database CalendarPropName::TimezoneServiceSet => CalendarProp::TimezoneServiceSet( @@ -191,23 +194,42 @@ impl Resource for CalendarResource { Ok(()) } CalendarProp::CalendarTimezone(timezone) => { - // TODO: Ensure that timezone-id is also updated - // We probably want to prohibit non-IANA timezones - self.cal.timezone = timezone; + if let Some(tz) = timezone { + // TODO: Proper error (calendar-timezone precondition) + let calendar = IcalParser::new(tz.as_bytes()) + .next() + .ok_or(rustical_dav::Error::BadRequest( + "No timezone data provided".to_owned(), + ))? + .map_err(|_| { + rustical_dav::Error::BadRequest( + "No timezone data provided".to_owned(), + ) + })?; + + let timezone = + calendar + .timezones + .first() + .ok_or(rustical_dav::Error::BadRequest( + "No timezone data provided".to_owned(), + ))?; + let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| { + rustical_dav::Error::BadRequest("No timezone data provided".to_owned()) + })?; + + self.cal.timezone_id = Some(timezone.name().to_owned()); + } Ok(()) } CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::CalendarTimezoneId(timezone_id) => { if let Some(tzid) = &timezone_id { - // Validate timezone id and set timezone accordingly - self.cal.timezone = Some( - vtimezones_rs::VTIMEZONES - .get(tzid) - .ok_or(rustical_dav::Error::BadRequest(format!( - "Invalid timezone-id: {tzid}" - )))? - .to_string(), - ); + if !vtimezones_rs::VTIMEZONES.contains_key(tzid) { + return Err(rustical_dav::Error::BadRequest(format!( + "Invalid timezone-id: {tzid}" + ))); + } } self.cal.timezone_id = timezone_id; Ok(()) @@ -248,15 +270,11 @@ impl Resource for CalendarResource { self.cal.description = None; Ok(()) } - CalendarPropName::CalendarTimezone => { - self.cal.timezone = None; - Ok(()) - } - CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly), - CalendarPropName::CalendarTimezoneId => { + CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => { self.cal.timezone_id = None; Ok(()) } + CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::CalendarOrder => { self.cal.order = 0; Ok(()) diff --git a/crates/frontend/public/templates/pages/calendar.html b/crates/frontend/public/templates/pages/calendar.html index 80fc9ae..19f9508 100644 --- a/crates/frontend/public/templates/pages/calendar.html +++ b/crates/frontend/public/templates/pages/calendar.html @@ -25,7 +25,7 @@ {% if let Some(timezone_id) = calendar.timezone_id %}

{{ timezone_id }}

{% endif %} -{% if let Some(timezone) = calendar.timezone %} +{% if let Some(timezone) = calendar.get_vtimezone() %} {% endif %} diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index a4566f9..20049b3 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -32,6 +32,7 @@ headers.workspace = true tower.workspace = true futures-core.workspace = true tower-sessions.workspace = true +vtimezones-rs.workspace = true [dev-dependencies] rstest = { workspace = true } diff --git a/crates/store/src/calendar.rs b/crates/store/src/calendar.rs index 96645d7..7b67580 100644 --- a/crates/store/src/calendar.rs +++ b/crates/store/src/calendar.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use crate::synctoken::format_synctoken; use chrono::NaiveDateTime; use rustical_ical::CalendarObjectType; @@ -11,7 +13,6 @@ pub struct Calendar { pub order: i64, pub description: Option, pub color: Option, - pub timezone: Option, pub timezone_id: Option, pub deleted_at: Option, pub synctoken: i64, @@ -24,4 +25,16 @@ impl Calendar { pub fn format_synctoken(&self) -> String { format_synctoken(self.synctoken) } + + pub fn get_timezone(&self) -> Option { + self.timezone_id + .as_ref() + .and_then(|tzid| chrono_tz::Tz::from_str(tzid).ok()) + } + + pub fn get_vtimezone(&self) -> Option<&'static str> { + self.timezone_id + .as_ref() + .and_then(|tzid| vtimezones_rs::VTIMEZONES.get(tzid).cloned()) + } } diff --git a/crates/store/src/contact_birthday_store.rs b/crates/store/src/contact_birthday_store.rs index cbfde40..32e4c2d 100644 --- a/crates/store/src/contact_birthday_store.rs +++ b/crates/store/src/contact_birthday_store.rs @@ -20,7 +20,6 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar { order: 0, description: None, color: None, - timezone: None, timezone_id: None, deleted_at: addressbook.deleted_at, synctoken: addressbook.synctoken, diff --git a/crates/store_sqlite/src/calendar_store.rs b/crates/store_sqlite/src/calendar_store.rs index 92e2e9a..4f70f1f 100644 --- a/crates/store_sqlite/src/calendar_store.rs +++ b/crates/store_sqlite/src/calendar_store.rs @@ -44,7 +44,6 @@ struct CalendarRow { order: i64, description: Option, color: Option, - timezone: Option, timezone_id: Option, deleted_at: Option, synctoken: i64, @@ -74,7 +73,6 @@ impl From for Calendar { order: value.order, description: value.description, color: value.color, - timezone: value.timezone, timezone_id: value.timezone_id, deleted_at: value.deleted_at, synctoken: value.synctoken, @@ -100,7 +98,7 @@ impl SqliteCalendarStore { ) -> Result { let cal = sqlx::query_as!( CalendarRow, - r#"SELECT * + r#"SELECT principal, id, displayname, "order", description, color, timezone_id, deleted_at, synctoken, subscription_url, push_topic, comp_event, comp_todo, comp_journal FROM calendars WHERE (principal, id) = (?, ?) AND ((deleted_at IS NULL) OR ?) "#, @@ -120,7 +118,7 @@ impl SqliteCalendarStore { ) -> Result, Error> { let cals = sqlx::query_as!( CalendarRow, - r#"SELECT * + r#"SELECT principal, id, displayname, "order", description, color, timezone_id, deleted_at, synctoken, subscription_url, push_topic, comp_event, comp_todo, comp_journal FROM calendars WHERE principal = ? AND deleted_at IS NULL"#, principal @@ -137,7 +135,7 @@ impl SqliteCalendarStore { ) -> Result, Error> { let cals = sqlx::query_as!( CalendarRow, - r#"SELECT * + r#"SELECT principal, id, displayname, "order", description, color, timezone_id, deleted_at, synctoken, subscription_url, push_topic, comp_event, comp_todo, comp_journal FROM calendars WHERE principal = ? AND deleted_at IS NOT NULL"#, principal @@ -157,8 +155,8 @@ impl SqliteCalendarStore { let comp_journal = calendar.components.contains(&CalendarObjectType::Journal); sqlx::query!( - r#"INSERT INTO calendars (principal, id, displayname, description, "order", color, subscription_url, timezone, timezone_id, push_topic, comp_event, comp_todo, comp_journal) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#, + r#"INSERT INTO calendars (principal, id, displayname, description, "order", color, subscription_url, timezone_id, push_topic, comp_event, comp_todo, comp_journal) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#, calendar.principal, calendar.id, calendar.displayname, @@ -166,7 +164,6 @@ impl SqliteCalendarStore { calendar.order, calendar.color, calendar.subscription_url, - calendar.timezone, calendar.timezone_id, calendar.push_topic, comp_event, comp_todo, comp_journal @@ -188,7 +185,7 @@ impl SqliteCalendarStore { let comp_journal = calendar.components.contains(&CalendarObjectType::Journal); let result = sqlx::query!( - r#"UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ? + r#"UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ? WHERE (principal, id) = (?, ?)"#, calendar.principal, calendar.id, @@ -196,7 +193,6 @@ impl SqliteCalendarStore { calendar.description, calendar.order, calendar.color, - calendar.timezone, calendar.timezone_id, calendar.push_topic, comp_event, comp_todo, comp_journal,