minicaldav/
api.rs

1// minicaldav: Small and easy CalDAV client.
2// Copyright (C) 2022 Florian Loers
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Main api of minicaldav.
18
19use std::collections::HashMap;
20
21use crate::caldav;
22use crate::ical;
23use crate::ical::Ical;
24#[cfg(feature = "serde")]
25use serde::{Deserialize, Serialize};
26use ureq::Agent;
27use url::Url;
28
29pub use crate::credentials::Credentials;
30
31/// Get all calendars from the given CalDAV endpoint.
32pub fn get_calendars(
33    agent: Agent,
34    credentials: &Credentials,
35    base_url: &Url,
36) -> Result<Vec<Calendar>, Error> {
37    let calendar_refs = caldav::get_calendars(agent, credentials, base_url)?;
38    let mut calendars = Vec::new();
39    for calendar_ref in calendar_refs {
40        calendars.push(Calendar {
41            base_url: base_url.clone(),
42            inner: calendar_ref,
43        });
44    }
45    Ok(calendars)
46}
47
48/// Get all todos in the given `Calendar`.
49/// This function returns a tuple of all todos that could be parsed and all todos that couldn't.
50/// If anything besides parsing the todo data fails, an Err will be returned.
51pub fn get_todos(
52    agent: Agent,
53    credentials: &Credentials,
54    calendar: &Calendar,
55) -> Result<(Vec<Event>, Vec<Error>), Error> {
56    let todo_refs = caldav::get_todos(agent, credentials, &calendar.base_url, &calendar.inner)?;
57    let mut todos = Vec::new();
58    let mut errors = Vec::new();
59    for todo_ref in todo_refs {
60        let lines = ical::LineIterator::new(&todo_ref.data);
61        match ical::Ical::parse(&lines) {
62            Ok(ical) => todos.push(Event {
63                url: todo_ref.url.clone(),
64                etag: todo_ref.etag.clone(),
65                ical,
66            }),
67            Err(e) => errors.push(Error::Ical(format!(
68                "Could not parse todo {}: {:?}",
69                todo_ref.data, e
70            ))),
71        }
72    }
73    Ok((todos, errors))
74}
75
76/// Get all events in the given `Calendar`.
77/// This function returns a tuple of all events that could be parsed and all events that couldn't.
78/// If anything besides parsing the event data fails, an Err will be returned.
79pub fn get_events(
80    agent: Agent,
81    credentials: &Credentials,
82    calendar: &Calendar,
83) -> Result<(Vec<Event>, Vec<Error>), Error> {
84    let event_refs = caldav::get_events(agent, credentials, &calendar.base_url, calendar.url())?;
85    let mut events = Vec::new();
86    let mut errors = Vec::new();
87    for event_ref in event_refs {
88        let lines = ical::LineIterator::new(&event_ref.data);
89        match ical::Ical::parse(&lines) {
90            Ok(ical) => events.push(Event {
91                url: event_ref.url.clone(),
92                etag: event_ref.etag.clone(),
93                ical,
94            }),
95            Err(e) => errors.push(Error::Ical(format!(
96                "Could not parse event {}: {:?}",
97                event_ref.data, e
98            ))),
99        }
100    }
101    Ok((events, errors))
102}
103
104/// Parses the given string into the Ical struct.
105pub fn parse_ical(raw: &str) -> Result<Ical, Error> {
106    let lines = ical::LineIterator::new(raw);
107    match ical::Ical::parse(&lines) {
108        Ok(ical) => Ok(ical),
109        Err(e) => Err(Error::Ical(format!("Could not parse event: {:?}", e))),
110    }
111}
112
113/// Save the given event on the CalDAV server.
114pub fn save_event(
115    agent: Agent,
116    credentials: &Credentials,
117    mut event: Event,
118) -> Result<Event, Error> {
119    for prop in &mut event.ical.properties {
120        if prop.name == "SEQUENCE" {
121            if let Ok(num) = prop.value.parse::<i64>() {
122                prop.value = format!("{}", num + 1);
123            }
124        }
125    }
126
127    let event_ref = caldav::EventRef {
128        data: event.ical.serialize(),
129        etag: None,
130        url: event.url,
131    };
132    let event_ref = caldav::save_event(agent, credentials, event_ref)?;
133    Ok(Event {
134        etag: event_ref.etag,
135        url: event_ref.url,
136        ..event
137    })
138}
139
140/// Remove the given event on the CalDAV server.
141pub fn remove_event(agent: Agent, credentials: &Credentials, event: Event) -> Result<(), Error> {
142    let event_ref = caldav::EventRef {
143        data: event.ical.serialize(),
144        etag: event.etag,
145        url: event.url,
146    };
147    caldav::remove_event(agent, credentials, event_ref)?;
148    Ok(())
149}
150
151/// A remote CalDAV calendar.
152#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
153#[derive(Debug, Clone)]
154pub struct Calendar {
155    base_url: Url,
156    inner: caldav::CalendarRef,
157}
158
159impl Calendar {
160    pub fn url(&self) -> &Url {
161        &self.inner.url
162    }
163    pub fn name(&self) -> &String {
164        &self.inner.name
165    }
166    pub fn color(&self) -> Option<&String> {
167        self.inner.color.as_ref()
168    }
169}
170
171#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
172/// An event in a CalDAV calendar.
173#[derive(Debug, Clone, Eq, PartialEq)]
174pub struct Event {
175    etag: Option<String>,
176    url: Url,
177    ical: ical::Ical,
178}
179
180impl Event {
181    /// Construct a new event.
182    pub fn new(etag: Option<String>, url: Url, ical: ical::Ical) -> Self {
183        Self { etag, url, ical }
184    }
185
186    /// The full url of this event.
187    pub fn url(&self) -> &Url {
188        &self.url
189    }
190
191    fn get_property(&self, name: &str, datatype: &str) -> Option<Property> {
192        self.ical.get(datatype).and_then(|ical| {
193            ical.properties.iter().find_map(|p| {
194                if p.name == name {
195                    Some(Property::from(p.clone()))
196                } else {
197                    None
198                }
199            })
200        })
201    }
202
203    /// Get the property of the given name or `None`.
204    pub fn pop_property(&mut self, name: &str) -> Option<Property> {
205        self.ical.get_mut("VEVENT").and_then(|ical| {
206            let index = ical.properties.iter().enumerate().find_map(|(i, p)| {
207                if p.name == name {
208                    Some(i)
209                } else {
210                    None
211                }
212            });
213
214            if let Some(index) = index {
215                Some(Property::from(ical.properties.remove(index)))
216            } else {
217                None
218            }
219        })
220    }
221
222    pub fn ical(&self) -> &Ical {
223        &self.ical
224    }
225
226    pub fn add(&mut self, property: Property) {
227        if let Some(ical) = self.ical.get_mut("VEVENT") {
228            ical.properties.push(property.into());
229        }
230    }
231
232    pub fn property(&self, name: &str) -> Option<Property> {
233        self.get_property(name, "VEVENT")
234    }
235
236    /// Get the property of the given name or `None`.
237    pub fn property_todo(&self, name: &str) -> Option<Property> {
238        self.get_property(name, "VTODO")
239    }
240
241    /// Get the value of the given property name or `None`.
242    pub fn get(&self, name: &str) -> Option<&String> {
243        self.ical.get("VEVENT").and_then(|ical| {
244            ical.properties
245                .iter()
246                .find_map(|p| if p.name == name { Some(&p.value) } else { None })
247        })
248    }
249
250    /// Set the value of the given property name or create a new property.
251    pub fn set(&mut self, name: &str, value: &str) {
252        match self
253            .ical
254            .get_mut("VEVENT")
255            .and_then(|e| e.properties.iter_mut().find(|p| p.name == name))
256        {
257            Some(p) => {
258                p.value = value.into();
259            }
260            None => self.add(Property::new(name, value)),
261        }
262    }
263
264    pub fn set_property_attribute(&mut self, name: &str, attr_name: &str, attr_value: &str) {
265        if let Some(p) = self
266            .ical
267            .get_mut("VEVENT")
268            .and_then(|e| e.properties.iter_mut().find(|p| p.name == name))
269        {
270            p.attributes.insert(attr_name.into(), attr_value.into());
271        }
272    }
273
274    /// Get all properties of this event.
275    fn get_properties(&self, datatype: &str) -> Vec<(&String, &String)> {
276        self.ical
277            .get(datatype)
278            .map(|ical| {
279                ical.properties
280                    .iter()
281                    .map(|p| (&p.name, &p.value))
282                    .collect::<Vec<(&String, &String)>>()
283            })
284            .unwrap_or_else(|| {
285                error!("Could not get properties: No VEVENT section.");
286                Vec::new()
287            })
288    }
289
290    pub fn into_properties(self) -> Vec<Property> {
291        for ical in self.ical.children {
292            if ical.name == "VEVENT" {
293                return ical.properties.into_iter().map(Property::from).collect();
294            }
295        }
296        Vec::new()
297    }
298
299    /// Get all properties of this event.
300    pub fn properties(&self) -> Vec<(&String, &String)> {
301        self.get_properties("VEVENT")
302    }
303
304    /// Get all properties of this todo.
305    pub fn properties_todo(&self) -> Vec<(&String, &String)> {
306        self.get_properties("VTODO")
307    }
308
309    pub fn etag(&self) -> Option<&String> {
310        self.etag.as_ref()
311    }
312
313    pub fn builder(url: Url) -> EventBuilder {
314        EventBuilder {
315            url,
316            etag: None,
317            properties: vec![],
318        }
319    }
320
321    pub fn set_etag(&mut self, etag: Option<String>) {
322        self.etag = etag
323    }
324}
325
326#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
327#[derive(Debug, Clone, PartialEq, Eq)]
328pub struct Property {
329    name: String,
330    value: String,
331    attributes: HashMap<String, String>,
332}
333
334impl Property {
335    pub fn new(name: &str, value: &str) -> Self {
336        Self {
337            name: name.into(),
338            value: value.into(),
339            attributes: Default::default(),
340        }
341    }
342    pub fn name(&self) -> &String {
343        &self.name
344    }
345
346    pub fn value(&self) -> &String {
347        &self.value
348    }
349
350    pub fn into_value(self) -> String {
351        self.value
352    }
353
354    pub fn attribute(&self, name: &str) -> Option<&String> {
355        self.attributes.get(name)
356    }
357}
358
359impl From<ical::Property> for Property {
360    fn from(p: ical::Property) -> Self {
361        Self {
362            name: p.name,
363            value: p.value,
364            attributes: p.attributes,
365        }
366    }
367}
368
369impl From<Property> for ical::Property {
370    fn from(p: Property) -> Self {
371        Self {
372            name: p.name,
373            value: p.value,
374            attributes: p.attributes,
375        }
376    }
377}
378
379/// Errors that may occur during minicalav operations.
380#[derive(Debug)]
381pub enum Error {
382    Ical(String),
383    Caldav(String),
384}
385
386impl std::error::Error for Error {}
387
388impl std::fmt::Display for Error {
389    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390        match self {
391            Error::Ical(msg) => write!(f, "CalDAV Error: '{}'", msg),
392            Error::Caldav(msg) => write!(f, "ICAL Error: '{}'", msg),
393        }
394    }
395}
396
397impl From<caldav::Error> for Error {
398    fn from(e: caldav::Error) -> Self {
399        Error::Ical(e.message)
400    }
401}
402
403impl From<ical::Error> for Error {
404    fn from(e: ical::Error) -> Self {
405        Error::Caldav(e.message)
406    }
407}
408
409#[derive(Debug)]
410pub struct EventBuilder {
411    url: Url,
412    etag: Option<String>,
413    properties: Vec<ical::Property>,
414}
415
416impl EventBuilder {
417    fn build_event(self, name: String) -> Event {
418        Event {
419            etag: self.etag,
420            url: self.url,
421            ical: ical::Ical {
422                name: "VCALENDAR".into(),
423                properties: vec![],
424                children: vec![ical::Ical {
425                    name,
426                    properties: self.properties,
427                    children: vec![],
428                }],
429            },
430        }
431    }
432
433    pub fn build(self) -> Event {
434        self.build_event("VEVENT".into())
435    }
436
437    pub fn build_todo(self) -> Event {
438        self.build_event("VTODO".into())
439    }
440
441    pub fn etag(mut self, etag: Option<String>) -> Self {
442        self.etag = etag;
443        self
444    }
445
446    pub fn uid(mut self, value: String) -> Self {
447        self.properties.push(ical::Property {
448            name: "UID".to_string(),
449            value,
450            attributes: HashMap::new(),
451        });
452        self
453    }
454
455    pub fn timestamp(mut self, value: String) -> Self {
456        self.properties.push(ical::Property {
457            name: "DTSTAMP".to_string(),
458            value,
459            attributes: HashMap::new(),
460        });
461        self
462    }
463
464    pub fn summary(mut self, value: String) -> Self {
465        self.properties.push(ical::Property {
466            name: "SUMMARY".to_string(),
467            value,
468            attributes: HashMap::new(),
469        });
470        self
471    }
472
473    pub fn priority(mut self, value: String) -> Self {
474        self.properties.push(ical::Property {
475            name: "PRIORITY".to_string(),
476            value,
477            attributes: HashMap::new(),
478        });
479        self
480    }
481
482    pub fn duedate(mut self, value: String) -> Self {
483        self.properties.push(ical::Property {
484            name: "DUE".to_string(),
485            value,
486            attributes: HashMap::new(),
487        });
488        self
489    }
490
491    pub fn status(mut self, value: String) -> Self {
492        self.properties.push(ical::Property {
493            name: "STATUS".to_string(),
494            value,
495            attributes: HashMap::new(),
496        });
497        self
498    }
499
500    pub fn generic(mut self, name: String, value: String) -> Self {
501        self.properties.push(ical::Property {
502            name,
503            value,
504            attributes: HashMap::new(),
505        });
506        self
507    }
508
509    pub fn location(mut self, value: Option<String>) -> Self {
510        if let Some(value) = value {
511            self.properties.push(ical::Property {
512                name: "LOCATION".to_string(),
513                value,
514                attributes: HashMap::new(),
515            });
516        }
517        self
518    }
519
520    pub fn start(mut self, value: String, attributes: Vec<(&str, &str)>) -> Self {
521        let mut attribs = HashMap::new();
522        for (k, v) in attributes {
523            attribs.insert(k.into(), v.into());
524        }
525        self.properties.push(ical::Property {
526            name: "DTSTART".to_string(),
527            value,
528            attributes: attribs,
529        });
530        self
531    }
532
533    pub fn end(mut self, value: String, attributes: Vec<(&str, &str)>) -> Self {
534        let mut attribs = HashMap::new();
535        for (k, v) in attributes {
536            attribs.insert(k.into(), v.into());
537        }
538        self.properties.push(ical::Property {
539            name: "DTEND".to_string(),
540            value,
541            attributes: attribs,
542        });
543        self
544    }
545
546    pub fn description(mut self, value: Option<String>) -> Self {
547        if let Some(value) = value {
548            self.properties.push(ical::Property {
549                name: "DESCRIPTION".to_string(),
550                value,
551                attributes: HashMap::new(),
552            });
553        }
554        self
555    }
556
557    pub fn rrule(mut self, value: Option<String>) -> Self {
558        if let Some(value) = value {
559            self.properties.push(ical::Property {
560                name: "RRULE".to_string(),
561                value,
562                attributes: HashMap::new(),
563            });
564        }
565        self
566    }
567}