Skip to main content

agentmail/client/
calendar.rs

1use crate::client::NoBody;
2use crate::client::scope::{InboxScope, Scoped};
3use crate::{Error, Page, types::*, util::urlish};
4
5/// The inbox's calendar. Every calendar endpoint is inbox-scoped; writes take
6/// an optional `etag` (from a prior read) for optimistic concurrency, and the
7/// API answers 428 when a newer revision exists.
8impl Scoped<'_, InboxScope<'_>> {
9    /// GET `{base}/calendar`, the inbox's calendar settings.
10    pub async fn get_calendar(&self, consistency: Option<Consistency>) -> Result<Calendar, Error> {
11        let query = consistency
12            .map(|c| vec![("consistency", c.as_str().to_string())])
13            .unwrap_or_default();
14        self.client
15            .request(
16                reqwest::Method::GET,
17                &format!("{}/calendar", self.base()),
18                &query,
19                None::<&NoBody>,
20            )
21            .await
22    }
23
24    /// PATCH `{base}/calendar`, move the calendar to another IANA timezone.
25    /// Pass the `etag` from a prior read to reject concurrent edits.
26    pub async fn update_calendar(
27        &self,
28        timezone: &str,
29        etag: Option<&str>,
30    ) -> Result<Calendar, Error> {
31        self.client
32            .request_with_headers(
33                reqwest::Method::PATCH,
34                &format!("{}/calendar", self.base()),
35                &[],
36                Some(&serde_json::json!({ "timezone": timezone })),
37                &Self::if_match(etag),
38            )
39            .await
40    }
41
42    /// GET `{base}/calendar/agenda`, occurrences in a window, flattened:
43    /// recurring series appear as their individual instances.
44    pub async fn get_agenda(&self, query: CalendarEventsQuery) -> Result<CalendarEventList, Error> {
45        self.client
46            .request(
47                reqwest::Method::GET,
48                &format!("{}/calendar/agenda", self.base()),
49                &query.query(),
50                None::<&NoBody>,
51            )
52            .await
53    }
54
55    /// Every occurrence in the query's window, draining pagination.
56    pub async fn list_all_agenda(
57        &self,
58        query: CalendarEventsQuery,
59    ) -> Result<Vec<CalendarEvent>, Error> {
60        let mut out = Vec::new();
61        let mut token = query.page_token.clone();
62        loop {
63            let page = token.clone();
64            let resp = self
65                .get_agenda(CalendarEventsQuery {
66                    page_token: page.clone(),
67                    limit: None,
68                    ..query.clone()
69                })
70                .await?;
71            let next = resp.next_page_token;
72            out.extend(resp.events);
73            // A server that keeps returning the same cursor would spin here
74            // forever; stop instead.
75            if next.is_some() && next == page {
76                return Ok(out);
77            }
78            match next {
79                Some(t) => token = Some(t),
80                None => return Ok(out),
81            }
82        }
83    }
84
85    /// GET `{base}/calendar/events`, one page of the calendar's stored events
86    /// (series stay unexpanded; use the agenda for flattened occurrences).
87    pub async fn list_calendar_events(&self, page: Page) -> Result<CalendarEventList, Error> {
88        self.client
89            .request(
90                reqwest::Method::GET,
91                &format!("{}/calendar/events", self.base()),
92                &page.query(),
93                None::<&NoBody>,
94            )
95            .await
96    }
97
98    /// Every stored event, draining pagination.
99    pub async fn list_all_calendar_events(&self) -> Result<Vec<CalendarEvent>, Error> {
100        let mut out = Vec::new();
101        let mut token = None;
102        loop {
103            let resp = self
104                .list_calendar_events(Page {
105                    limit: None,
106                    page_token: token,
107                })
108                .await?;
109            let next = resp.next_page_token;
110            out.extend(resp.events);
111            match next {
112                Some(t) => token = Some(t),
113                None => return Ok(out),
114            }
115        }
116    }
117
118    /// POST `{base}/calendar/events`, create an event (or series, with a
119    /// `recurrence` rule).
120    pub async fn create_calendar_event(
121        &self,
122        event: CreateCalendarEvent,
123    ) -> Result<CalendarEventMutation, Error> {
124        self.client
125            .request(
126                reqwest::Method::POST,
127                &format!("{}/calendar/events", self.base()),
128                &[],
129                Some(&event),
130            )
131            .await
132    }
133
134    /// GET `{base}/calendar/events/{event_id}`.
135    pub async fn get_calendar_event(
136        &self,
137        event_id: &str,
138        consistency: Option<Consistency>,
139    ) -> Result<CalendarEvent, Error> {
140        let query = consistency
141            .map(|c| vec![("consistency", c.as_str().to_string())])
142            .unwrap_or_default();
143        self.client
144            .request(
145                reqwest::Method::GET,
146                &format!("{}/calendar/events/{}", self.base(), urlish(event_id)),
147                &query,
148                None::<&NoBody>,
149            )
150            .await
151    }
152
153    /// PATCH `{base}/calendar/events/{event_id}`. On a series instance, pass
154    /// `mode` to move just that occurrence or it and every later one.
155    pub async fn update_calendar_event(
156        &self,
157        event_id: &str,
158        update: UpdateCalendarEvent,
159        mode: Option<MutationMode>,
160        etag: Option<&str>,
161    ) -> Result<CalendarEventMutation, Error> {
162        let mut query = Vec::new();
163        if let Some(mode) = mode {
164            query.push(("mode", mode.as_str().to_string()));
165        }
166        self.client
167            .request_with_headers(
168                reqwest::Method::PATCH,
169                &format!("{}/calendar/events/{}", self.base(), urlish(event_id)),
170                &query,
171                Some(&update),
172                &Self::if_match(etag),
173            )
174            .await
175    }
176
177    /// DELETE `{base}/calendar/events/{event_id}`. On a series instance,
178    /// `mode` picks between cancelling just that occurrence and every future
179    /// one.
180    pub async fn delete_calendar_event(
181        &self,
182        event_id: &str,
183        mode: Option<MutationMode>,
184        send_invites: Option<bool>,
185        etag: Option<&str>,
186    ) -> Result<DeleteCalendarEventResult, Error> {
187        let mut query = Vec::new();
188        if let Some(mode) = mode {
189            query.push(("mode", mode.as_str().to_string()));
190        }
191        if let Some(send) = send_invites {
192            query.push(("send_invites", send.to_string()));
193        }
194        self.client
195            .request_with_headers(
196                reqwest::Method::DELETE,
197                &format!("{}/calendar/events/{}", self.base(), urlish(event_id)),
198                &query,
199                None::<&NoBody>,
200                &Self::if_match(etag),
201            )
202            .await
203    }
204
205    /// GET `{base}/calendar/events/{event_id}/instances`, the occurrences of
206    /// a recurring event in a window.
207    pub async fn list_calendar_event_instances(
208        &self,
209        event_id: &str,
210        query: CalendarEventsQuery,
211    ) -> Result<CalendarEventList, Error> {
212        self.client
213            .request(
214                reqwest::Method::GET,
215                &format!(
216                    "{}/calendar/events/{}/instances",
217                    self.base(),
218                    urlish(event_id)
219                ),
220                &query.query(),
221                None::<&NoBody>,
222            )
223            .await
224    }
225
226    /// Every instance of a recurring event in the query's window, draining
227    /// pagination.
228    pub async fn list_all_calendar_event_instances(
229        &self,
230        event_id: &str,
231        query: CalendarEventsQuery,
232    ) -> Result<Vec<CalendarEvent>, Error> {
233        let mut out = Vec::new();
234        let mut token = query.page_token.clone();
235        loop {
236            let page = token.clone();
237            let resp = self
238                .list_calendar_event_instances(
239                    event_id,
240                    CalendarEventsQuery {
241                        page_token: page.clone(),
242                        limit: None,
243                        ..query.clone()
244                    },
245                )
246                .await?;
247            let next = resp.next_page_token;
248            out.extend(resp.events);
249            // Same non-advancing-cursor guard as `list_all_agenda`.
250            if next.is_some() && next == page {
251                return Ok(out);
252            }
253            match next {
254                Some(t) => token = Some(t),
255                None => return Ok(out),
256            }
257        }
258    }
259
260    /// POST `{base}/calendar/events/{event_id}/respond`, reply to an invite
261    /// on behalf of this inbox.
262    pub async fn respond_to_calendar_event(
263        &self,
264        event_id: &str,
265        respond: RespondToCalendarEvent,
266        etag: Option<&str>,
267    ) -> Result<CalendarEventMutation, Error> {
268        self.client
269            .request_with_headers(
270                reqwest::Method::POST,
271                &format!(
272                    "{}/calendar/events/{}/respond",
273                    self.base(),
274                    urlish(event_id)
275                ),
276                &[],
277                Some(&respond),
278                &Self::if_match(etag),
279            )
280            .await
281    }
282
283    fn if_match(etag: Option<&str>) -> Vec<(&'static str, String)> {
284        etag.map(|e| vec![("If-Match", e.to_string())])
285            .unwrap_or_default()
286    }
287}