1use std::collections::BTreeMap;
7
8use chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc};
9use serde::Deserialize;
10use serde_json::{Value, json};
11
12use crate::{CALENDARS, Client, Error, Result, ids_of};
13
14#[derive(Debug, Clone, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct Calendar {
17 pub id: String,
18 pub name: String,
19 #[serde(default)]
20 pub color: Option<String>,
21 #[serde(default)]
22 pub is_visible: Option<bool>,
23 #[serde(default)]
24 pub my_rights: Option<CalendarRights>,
25 #[serde(skip)]
27 pub account: String,
28 #[serde(skip)]
30 pub shared: Option<String>,
31}
32
33#[derive(Debug, Clone, Copy, Default, Deserialize)]
34#[serde(rename_all = "camelCase", default)]
35pub struct CalendarRights {
36 pub may_write_all: bool,
37 #[serde(rename = "mayRSVP")]
38 pub may_rsvp: bool,
39}
40
41impl Calendar {
42 pub fn writable(&self) -> bool {
44 self.my_rights.is_none_or(|rights| rights.may_write_all)
45 }
46}
47
48#[derive(Debug, Clone, Default, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct Participant {
51 #[serde(default)]
52 pub name: Option<String>,
53 #[serde(default)]
54 pub calendar_address: Option<String>,
55 #[serde(default)]
56 pub participation_status: Option<String>,
57}
58
59#[derive(Debug, Clone, Default, Deserialize)]
60pub struct Location {
61 #[serde(default)]
62 pub name: Option<String>,
63}
64
65#[derive(Debug, Clone, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct Event {
69 pub id: String,
70 #[serde(default)]
72 pub base_event_id: Option<String>,
73 #[serde(default)]
75 pub recurrence_id: Option<String>,
76 #[serde(default)]
78 pub recurrence_overrides: Option<serde_json::Map<String, Value>>,
79 #[serde(default)]
80 pub organizer_calendar_address: Option<String>,
81 #[serde(default)]
83 pub status: Option<String>,
84 #[serde(default)]
85 pub calendar_ids: BTreeMap<String, bool>,
86 #[serde(default)]
87 pub title: Option<String>,
88 #[serde(default)]
89 pub description: Option<String>,
90 #[serde(default)]
91 pub start: Option<String>,
92 #[serde(default)]
93 pub time_zone: Option<String>,
94 #[serde(default)]
95 pub duration: Option<String>,
96 #[serde(default)]
97 pub show_without_time: bool,
98 #[serde(default)]
99 pub utc_start: Option<String>,
100 #[serde(default)]
101 pub utc_end: Option<String>,
102 #[serde(default)]
103 pub recurrence_rule: Option<Value>,
104 #[serde(default)]
105 pub locations: Option<BTreeMap<String, Location>>,
106 #[serde(default)]
107 pub participants: Option<BTreeMap<String, Participant>>,
108 #[serde(skip)]
109 pub account: String,
110}
111
112impl Event {
113 pub fn starts(&self) -> Option<DateTime<Utc>> {
115 if let Some(start) = self.utc_start.as_deref().and_then(|s| DateTime::parse_from_rfc3339(s).ok()) {
116 return Some(start.with_timezone(&Utc));
117 }
118 let start = self.start.as_deref()?;
119 chrono::NaiveDateTime::parse_from_str(start, "%Y-%m-%dT%H:%M:%S").ok().map(|naive| naive.and_utc())
120 }
121
122 pub fn ends(&self) -> Option<DateTime<Utc>> {
123 self.utc_end.as_deref().and_then(|s| DateTime::parse_from_rfc3339(s).ok()).map(|end| end.with_timezone(&Utc))
124 }
125
126 pub fn is_cancelled(&self) -> bool {
127 self.status.as_deref() == Some("cancelled")
128 }
129
130 pub fn event_id(&self) -> &str {
132 self.base_event_id.as_deref().unwrap_or(&self.id)
133 }
134
135 pub fn me<'a>(&'a self, addresses: &[String]) -> Option<(&'a str, &'a Participant)> {
137 self.participants.as_ref()?.iter().map(|(key, p)| (key.as_str(), p)).find(|(_, participant)| {
138 let address = participant.calendar_address.as_deref().unwrap_or_default();
139 let address = address.strip_prefix("mailto:").unwrap_or(address);
140 addresses.iter().any(|own| own.eq_ignore_ascii_case(address))
141 })
142 }
143
144 pub fn organized_by(&self, addresses: &[String]) -> bool {
146 match self.organizer_calendar_address.as_deref() {
147 None => true,
148 Some(organizer) => {
149 let organizer = organizer.strip_prefix("mailto:").unwrap_or(organizer);
150 addresses.iter().any(|own| own.eq_ignore_ascii_case(organizer))
151 }
152 }
153 }
154
155 pub fn location(&self) -> Option<String> {
156 let names: Vec<_> = self.locations.as_ref()?.values().filter_map(|location| location.name.clone()).collect();
157 (!names.is_empty()).then(|| names.join(", "))
158 }
159}
160
161#[derive(Deserialize)]
162struct List<T> {
163 list: Vec<T>,
164}
165
166fn list<T: serde::de::DeserializeOwned>(value: Option<&Value>) -> Result<Vec<T>> {
167 let value = value.cloned().unwrap_or(json!({ "list": [] }));
168 serde_json::from_value::<List<T>>(value).map(|list| list.list).map_err(|error| Error::Protocol(error.to_string()))
169}
170
171pub async fn calendars(client: &Client) -> Result<Vec<Calendar>> {
173 let mut out = Vec::new();
174 for (id, account) in client.session.accounts_with(CALENDARS) {
175 let result = client.call(&[CALENDARS], "Calendar/get", json!({ "accountId": id })).await?;
176 for mut calendar in list::<Calendar>(Some(&result))? {
177 calendar.account = id.to_owned();
178 calendar.shared = (!account.is_personal).then(|| account.name.clone());
179 out.push(calendar);
180 }
181 }
182 Ok(out)
183}
184
185fn utc(time: DateTime<Utc>) -> String {
186 time.to_rfc3339_opts(SecondsFormat::Secs, true)
187}
188
189pub async fn event(client: &Client, account: &str, id: &str) -> Result<Option<Event>> {
191 let result = client.call(&[CALENDARS], "CalendarEvent/get", json!({ "accountId": account, "ids": [id] })).await?;
192 Ok(list::<Event>(Some(&result))?.into_iter().next().map(|mut event| {
193 event.account = account.to_owned();
194 event
195 }))
196}
197
198pub async fn instances(
200 client: &Client,
201 calendars: &[Calendar],
202 after: DateTime<Utc>,
203 before: DateTime<Utc>,
204) -> Result<Vec<Event>> {
205 let mut accounts: Vec<&str> = calendars.iter().map(|calendar| calendar.account.as_str()).collect();
206 accounts.dedup();
207 let mut out = Vec::new();
208 for account in accounts {
209 let visible: Vec<&str> = calendars
210 .iter()
211 .filter(|calendar| calendar.account == account && calendar.is_visible.unwrap_or(true))
212 .map(|calendar| calendar.id.as_str())
213 .collect();
214 let responses = client
215 .request(
216 &[CALENDARS],
217 vec![
218 (
219 "CalendarEvent/query",
220 json!({
221 "accountId": account, "expandRecurrences": true,
222 "filter": { "after": utc(after), "before": utc(before) },
223 }),
224 "q",
225 ),
226 (
227 "CalendarEvent/get",
228 json!({ "accountId": account, "#ids": ids_of("q", "CalendarEvent/query") }),
229 "g",
230 ),
231 ],
232 )
233 .await?;
234 for mut event in list::<Event>(responses.get("g"))? {
235 if event.calendar_ids.keys().any(|id| visible.contains(&id.as_str())) {
236 event.account = account.to_owned();
237 out.push(event);
238 }
239 }
240 }
241 out.sort_by_key(Event::starts);
242 Ok(out)
243}
244
245pub fn parse_duration(text: &str) -> Option<i64> {
247 let text = text.strip_prefix('P')?;
248 let (date, time) = text.split_once('T').unwrap_or((text, ""));
249 let mut seconds = 0i64;
250 let mut take = |part: &str, units: &[(char, i64)]| -> Option<()> {
251 let mut number = String::new();
252 for c in part.chars() {
253 if c.is_ascii_digit() {
254 number.push(c);
255 } else {
256 let factor = units.iter().find(|(unit, _)| *unit == c)?.1;
257 seconds += number.parse::<i64>().ok()? * factor;
258 number.clear();
259 }
260 }
261 number.is_empty().then_some(())
262 };
263 take(date, &[('W', 7 * 86_400), ('D', 86_400)])?;
264 take(time, &[('H', 3_600), ('M', 60), ('S', 1)])?;
265 Some(seconds)
266}
267
268pub fn format_duration(seconds: i64) -> String {
270 let seconds = seconds.max(0);
271 let (days, rest) = (seconds / 86_400, seconds % 86_400);
272 let (hours, minutes) = (rest / 3_600, rest % 3_600 / 60);
273 let mut out = String::from("P");
274 if days > 0 {
275 out.push_str(&format!("{days}D"));
276 }
277 if hours > 0 || minutes > 0 || days == 0 {
278 out.push('T');
279 if hours > 0 {
280 out.push_str(&format!("{hours}H"));
281 }
282 if minutes > 0 || hours == 0 {
283 out.push_str(&format!("{minutes}M"));
284 }
285 }
286 out
287}
288
289pub const FREQUENCIES: [&str; 4] = ["daily", "weekly", "monthly", "yearly"];
291
292#[derive(Debug, Clone, PartialEq, Eq)]
294pub struct EventForm {
295 pub title: String,
296 pub all_day: bool,
297 pub start: NaiveDateTime,
299 pub end: NaiveDateTime,
300 pub time_zone: Option<String>,
301 pub location: String,
302 pub description: String,
303 pub participants: Vec<String>,
305 pub repeat: Option<String>,
307 pub answers: Vec<(String, String)>,
310 pub was: Option<(NaiveDateTime, NaiveDateTime, bool)>,
311}
312
313impl EventForm {
314 pub fn of(event: &Event, own: &[String]) -> Option<Self> {
316 let start = NaiveDateTime::parse_from_str(event.start.as_deref()?, "%Y-%m-%dT%H:%M:%S").ok()?;
317 let seconds = event.duration.as_deref().and_then(parse_duration).unwrap_or(if event.show_without_time {
318 86_400
319 } else {
320 3_600
321 });
322 let mut end = start + chrono::Duration::seconds(seconds);
323 if event.show_without_time {
324 end -= chrono::Duration::days(1);
325 }
326 let participants = event
327 .participants
328 .iter()
329 .flat_map(|map| map.values())
330 .filter_map(|p| p.calendar_address.as_deref())
331 .map(|address| address.strip_prefix("mailto:").unwrap_or(address).to_owned())
332 .filter(|address| !own.iter().any(|o| o.eq_ignore_ascii_case(address)))
333 .collect();
334 let answers = event
335 .participants
336 .iter()
337 .flat_map(|map| map.values())
338 .filter_map(|p| {
339 let address = p.calendar_address.as_deref()?;
340 let address = address.strip_prefix("mailto:").unwrap_or(address).to_lowercase();
341 Some((address, p.participation_status.clone()?))
342 })
343 .collect();
344 let repeat = event
345 .recurrence_rule
346 .as_ref()
347 .and_then(|rule| rule.get("frequency"))
348 .and_then(Value::as_str)
349 .map(str::to_owned);
350 Some(Self {
351 title: event.title.clone().unwrap_or_default(),
352 all_day: event.show_without_time,
353 start,
354 end,
355 time_zone: event.time_zone.clone(),
356 location: event.location().unwrap_or_default(),
357 description: event.description.clone().unwrap_or_default(),
358 participants,
359 repeat,
360 answers,
361 was: Some((start, end, event.show_without_time)),
362 })
363 }
364
365 pub fn object(&self, own: &str) -> Value {
367 let (start, seconds) = if self.all_day {
368 let days = (self.end.date() - self.start.date()).num_days().max(0) + 1;
369 (self.start.date().and_hms_opt(0, 0, 0).unwrap_or(self.start), days * 86_400)
370 } else {
371 (self.start, (self.end - self.start).num_seconds())
372 };
373 let mut value = json!({
374 "@type": "Event",
375 "title": self.title,
376 "start": start.format("%Y-%m-%dT%H:%M:%S").to_string(),
377 "duration": format_duration(seconds),
378 "showWithoutTime": self.all_day,
379 "timeZone": if self.all_day { Value::Null } else { json!(self.time_zone) },
380 "description": if self.description.is_empty() { Value::Null } else { json!(self.description) },
381 "locations": if self.location.is_empty() { Value::Null } else {
382 json!({ "l1": { "@type": "Location", "name": self.location } })
383 },
384 "recurrenceRule": match &self.repeat {
385 Some(frequency) => json!({ "@type": "RecurrenceRule", "frequency": frequency }),
386 None => Value::Null,
387 },
388 "participants": Value::Null,
389 "organizerCalendarAddress": Value::Null,
390 });
391 let others: Vec<String> = self
392 .participants
393 .iter()
394 .map(|address| address.trim().to_lowercase())
395 .filter(|address| !address.is_empty() && !address.eq_ignore_ascii_case(own))
396 .collect();
397 if !others.is_empty() {
398 let mut participants = serde_json::Map::new();
399 participants.insert(
400 "p0".into(),
401 json!({ "calendarAddress": format!("mailto:{own}"), "roles": { "owner": true, "attendee": true },
402 "participationStatus": "accepted" }),
403 );
404 let same_time = self.was == Some((self.start, self.end, self.all_day));
405 for (index, address) in others.iter().enumerate() {
406 let status = self
407 .answers
408 .iter()
409 .find(|(known, _)| same_time && known == address)
410 .map_or("needs-action", |(_, status)| status.as_str());
411 participants.insert(
412 format!("p{}", index + 1),
413 json!({ "calendarAddress": format!("mailto:{address}"), "roles": { "attendee": true },
414 "participationStatus": status, "expectReply": true }),
415 );
416 }
417 value["participants"] = Value::Object(participants);
418 value["organizerCalendarAddress"] = json!(format!("mailto:{own}"));
419 }
420 value
421 }
422}
423
424const SCHEDULE: &str = "sendSchedulingMessages";
425
426pub async fn create(client: &Client, calendar: &Calendar, form: &EventForm, own: &str) -> Result<String> {
428 let mut object = form.object(own);
429 object["calendarIds"] = json!({ calendar.id.as_str(): true });
430 let result = client
431 .set(
432 &[CALENDARS],
433 "CalendarEvent",
434 &calendar.account,
435 json!({ "create": { "e": object } }),
436 json!({ SCHEDULE: true }),
437 )
438 .await?;
439 result
440 .pointer("/created/e/id")
441 .and_then(Value::as_str)
442 .map(str::to_owned)
443 .ok_or_else(|| Error::Protocol("the event has no id".into()))
444}
445
446pub async fn update(client: &Client, calendar: &Calendar, id: &str, form: &EventForm, own: &str) -> Result<()> {
448 let mut object = form.object(own);
449 object["calendarIds"] = json!({ calendar.id.as_str(): true });
450 let change = json!({ "update": { id: object } });
451 client.set(&[CALENDARS], "CalendarEvent", &calendar.account, change, json!({ SCHEDULE: true })).await?;
452 Ok(())
453}
454
455pub async fn raw_event(client: &Client, account: &str, id: &str) -> Result<serde_json::Map<String, Value>> {
457 let result = client.call(&[CALENDARS], "CalendarEvent/get", json!({ "accountId": account, "ids": [id] })).await?;
458 match result.pointer("/list/0") {
459 Some(Value::Object(event)) => Ok(event.clone()),
460 _ => Err(Error::Protocol("the event is gone".into())),
461 }
462}
463
464pub async fn import(
467 client: &Client,
468 calendar: &Calendar,
469 events: Vec<serde_json::Map<String, Value>>,
470) -> Result<(usize, Vec<String>)> {
471 let create: serde_json::Map<String, Value> = events
472 .into_iter()
473 .enumerate()
474 .map(|(n, mut event)| {
475 event.insert("calendarIds".into(), json!({ calendar.id.as_str(): true }));
476 (format!("i{n}"), Value::Object(event))
477 })
478 .collect();
479 let result = client
480 .call(
481 &[CALENDARS],
482 "CalendarEvent/set",
483 json!({ "accountId": calendar.account, "create": create, SCHEDULE: false }),
484 )
485 .await?;
486 Ok(made_and_refused(&result))
487}
488
489pub(crate) fn made_and_refused(result: &Value) -> (usize, Vec<String>) {
491 let made = result.get("created").and_then(Value::as_object).map_or(0, serde_json::Map::len);
492 let refused = result
493 .get("notCreated")
494 .and_then(Value::as_object)
495 .map(|failed| {
496 failed
497 .values()
498 .map(|error| {
499 let kind = error["type"].as_str().unwrap_or("refused");
500 error["description"].as_str().map_or_else(|| kind.to_owned(), |text| format!("{kind}: {text}"))
501 })
502 .collect()
503 })
504 .unwrap_or_default();
505 (made, refused)
506}
507
508const OVERRIDDEN: [&str; 6] = ["title", "start", "duration", "showWithoutTime", "description", "locations"];
510
511pub async fn update_instance(
513 client: &Client,
514 series: &Event,
515 recurrence_id: &str,
516 form: &EventForm,
517 own: &str,
518) -> Result<()> {
519 let whole = form.object(own);
520 let mut patch: serde_json::Map<String, Value> =
521 OVERRIDDEN.iter().map(|key| ((*key).to_owned(), whole[*key].clone())).collect();
522 if whole["timeZone"] != json!(series.time_zone) {
523 patch.insert("timeZone".into(), whole["timeZone"].clone());
524 }
525 override_instance(client, series, recurrence_id, Value::Object(patch)).await
526}
527
528pub async fn destroy_instance(client: &Client, series: &Event, recurrence_id: &str) -> Result<()> {
530 override_instance(client, series, recurrence_id, json!({ "excluded": true })).await
531}
532
533async fn override_instance(client: &Client, series: &Event, recurrence_id: &str, patch: Value) -> Result<()> {
534 let mut overrides = series.recurrence_overrides.clone().unwrap_or_default();
535 overrides.insert(recurrence_id.to_owned(), patch);
536 let change = json!({ "update": { series.id.as_str(): { "recurrenceOverrides": overrides } } });
537 client.set(&[CALENDARS], "CalendarEvent", &series.account, change, json!({ SCHEDULE: true })).await?;
538 Ok(())
539}
540
541pub async fn destroy(client: &Client, account: &str, id: &str) -> Result<()> {
543 client.set(&[CALENDARS], "CalendarEvent", account, json!({ "destroy": [id] }), json!({ SCHEDULE: true })).await?;
544 Ok(())
545}
546
547pub async fn answer(client: &Client, account: &str, id: &str, key: &str, status: &str) -> Result<()> {
549 let path = format!("participants/{key}/participationStatus");
550 let change = json!({ "update": { id: { path: status } } });
551 client.set(&[CALENDARS], "CalendarEvent", account, change, json!({ SCHEDULE: true })).await?;
552 Ok(())
553}
554
555pub fn local_time_zone() -> Option<String> {
557 iana_time_zone::get_timezone().ok()
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563
564 #[test]
565 fn durations() {
566 assert_eq!(parse_duration("PT1H30M"), Some(5_400));
567 assert_eq!(parse_duration("P1D"), Some(86_400));
568 assert_eq!(parse_duration("P1W"), Some(604_800));
569 assert_eq!(parse_duration("P1DT2H"), Some(93_600));
570 assert_eq!(parse_duration("1H"), None);
571 assert_eq!(parse_duration("PT1X"), None);
572 assert_eq!(format_duration(5_400), "PT1H30M");
573 assert_eq!(format_duration(3_600), "PT1H");
574 assert_eq!(format_duration(172_800), "P2D");
575 assert_eq!(format_duration(0), "PT0M");
576 }
577
578 #[test]
579 fn forms() {
580 let at = |s: &str| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M").unwrap();
581 let form = EventForm {
582 title: "Plans".into(),
583 all_day: false,
584 start: at("2026-10-29 10:00"),
585 end: at("2026-10-29 11:30"),
586 time_zone: Some("Europe/Berlin".into()),
587 location: "Room 1".into(),
588 description: String::new(),
589 participants: vec!["Bob@x.example".into(), "me@x.example".into()],
590 repeat: Some("weekly".into()),
591 answers: vec![("bob@x.example".into(), "accepted".into())],
592 was: Some((at("2026-10-29 10:00"), at("2026-10-29 11:30"), false)),
593 };
594 assert_eq!(form.object("me@x.example")["participants"]["p1"]["participationStatus"], "accepted");
596 let moved = EventForm { start: at("2026-10-29 10:30"), ..form.clone() };
597 assert_eq!(moved.object("me@x.example")["participants"]["p1"]["participationStatus"], "needs-action");
598 let object = form.object("me@x.example");
599 assert_eq!(object["start"], "2026-10-29T10:00:00");
600 assert_eq!(object["duration"], "PT1H30M");
601 assert_eq!(object["timeZone"], "Europe/Berlin");
602 assert_eq!(object["recurrenceRule"]["frequency"], "weekly");
603 assert_eq!(object["participants"]["p1"]["calendarAddress"], "mailto:bob@x.example");
604 assert!(object["participants"].get("p2").is_none());
605 assert_eq!(object["organizerCalendarAddress"], "mailto:me@x.example");
606 let all_day = EventForm { all_day: true, end: at("2026-10-30 00:00"), participants: vec![], ..form };
607 let object = all_day.object("me@x.example");
608 assert_eq!((object["duration"].as_str(), object["timeZone"].clone()), (Some("P2D"), Value::Null));
609 assert_eq!(object["participants"], Value::Null);
610 }
611}