Skip to main content

pidge_client/graph/
mod.rs

1//! Microsoft Graph API client.
2
3mod calendars;
4pub mod events;
5mod mail;
6mod me;
7
8pub use calendars::list_calendars;
9pub use events::{
10    EventsPage, NewEvent, RsvpKind, cancel_event, create_event, delete_event, get_event,
11    list_calendar_view, move_event_to_calendar, move_time, rsvp_event, search_events, update_event,
12};
13pub use mail::{
14    InboxPage, Outgoing, add_attachment, create_draft, create_forward_draft,
15    create_reply_all_draft, create_reply_draft, delete_attachment, delete_message,
16    fetch_message_headers, forward_message, get_attachment_bytes, get_message, list_attachments,
17    list_drafts, list_inbox, mark_read, mark_unread, move_message, reply_all_message,
18    reply_message, search_messages, send_draft, send_mail, set_flag, update_draft,
19};
20pub use me::{Me, get_me};
21
22use crate::auth::AuthClient;
23use crate::auth::config;
24use crate::error::ClientError;
25use pidge_core::Message;
26
27/// Stateful Microsoft Graph client. Holds an AuthClient and a shared HTTP client.
28pub struct GraphClient {
29    auth: AuthClient,
30    http: reqwest::Client,
31    base_url: String,
32}
33
34impl GraphClient {
35    pub fn new(auth: AuthClient) -> Result<Self, ClientError> {
36        Ok(Self {
37            auth,
38            http: reqwest::Client::builder()
39                .user_agent(format!("pidge/{}", env!("CARGO_PKG_VERSION")))
40                .build()?,
41            base_url: config::GRAPH_BASE.to_string(),
42        })
43    }
44
45    pub fn for_test(auth: AuthClient, base_url: impl Into<String>) -> Self {
46        Self {
47            auth,
48            http: reqwest::Client::new(),
49            base_url: base_url.into(),
50        }
51    }
52
53    pub fn auth(&self) -> &AuthClient {
54        &self.auth
55    }
56
57    /// GET /me. Used right after sign-in to learn the user's email.
58    pub async fn me(&self, access_token: &str) -> Result<Me, ClientError> {
59        get_me(&self.http, &self.base_url, access_token).await
60    }
61
62    /// GET /me/mailFolders/inbox/messages for a given account email.
63    /// Acquires/refreshes a token transparently via `AuthClient::get_valid_token`.
64    pub async fn list_inbox(
65        &self,
66        account: &str,
67        limit: usize,
68        skip: usize,
69        unread_only: bool,
70    ) -> Result<InboxPage, ClientError> {
71        let token = self.auth.get_valid_token(account).await?;
72        list_inbox(
73            &self.http,
74            &self.base_url,
75            &token,
76            account,
77            limit,
78            skip,
79            unread_only,
80        )
81        .await
82    }
83
84    /// GET /me/messages with `$search="<query>"` for a given account.
85    pub async fn search_messages(
86        &self,
87        account: &str,
88        query: &str,
89        limit: usize,
90    ) -> Result<Vec<Message>, ClientError> {
91        let token = self.auth.get_valid_token(account).await?;
92        search_messages(&self.http, &self.base_url, &token, account, query, limit).await
93    }
94
95    /// PATCH /me/messages/{id} with `{ "isRead": false }`.
96    pub async fn mark_unread(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
97        let token = self.auth.get_valid_token(account).await?;
98        mail::mark_unread(&self.http, &self.base_url, &token, message_id).await
99    }
100
101    /// PATCH /me/messages/{id} with `{ "flag": { "flagStatus": "flagged"|"notFlagged" } }`.
102    pub async fn set_flag(
103        &self,
104        account: &str,
105        message_id: &str,
106        flagged: bool,
107    ) -> Result<(), ClientError> {
108        let token = self.auth.get_valid_token(account).await?;
109        mail::set_flag(&self.http, &self.base_url, &token, message_id, flagged).await
110    }
111
112    /// POST /me/messages/{id}/move — move to a folder by ID or well-known name.
113    pub async fn move_message(
114        &self,
115        account: &str,
116        message_id: &str,
117        destination: &str,
118    ) -> Result<(), ClientError> {
119        let token = self.auth.get_valid_token(account).await?;
120        mail::move_message(&self.http, &self.base_url, &token, message_id, destination).await
121    }
122
123    /// POST /me/sendMail — compose-and-send a new message.
124    pub async fn send_mail(&self, account: &str, message: &Outgoing) -> Result<(), ClientError> {
125        let token = self.auth.get_valid_token(account).await?;
126        mail::send_mail(&self.http, &self.base_url, &token, message).await
127    }
128
129    /// POST /me/messages/{id}/reply.
130    pub async fn reply_message(
131        &self,
132        account: &str,
133        message_id: &str,
134        comment: &str,
135    ) -> Result<(), ClientError> {
136        let token = self.auth.get_valid_token(account).await?;
137        mail::reply_message(&self.http, &self.base_url, &token, message_id, comment).await
138    }
139
140    /// POST /me/messages/{id}/replyAll.
141    pub async fn reply_all_message(
142        &self,
143        account: &str,
144        message_id: &str,
145        comment: &str,
146    ) -> Result<(), ClientError> {
147        let token = self.auth.get_valid_token(account).await?;
148        mail::reply_all_message(&self.http, &self.base_url, &token, message_id, comment).await
149    }
150
151    /// POST /me/messages/{id}/forward.
152    pub async fn forward_message(
153        &self,
154        account: &str,
155        message_id: &str,
156        to: &[String],
157        comment: &str,
158    ) -> Result<(), ClientError> {
159        let token = self.auth.get_valid_token(account).await?;
160        mail::forward_message(&self.http, &self.base_url, &token, message_id, to, comment).await
161    }
162
163    /// GET /me/mailFolders/drafts/messages.
164    pub async fn list_drafts(
165        &self,
166        account: &str,
167        limit: usize,
168        skip: usize,
169    ) -> Result<InboxPage, ClientError> {
170        let token = self.auth.get_valid_token(account).await?;
171        mail::list_drafts(&self.http, &self.base_url, &token, account, limit, skip).await
172    }
173
174    /// POST /me/messages — create a draft, returning its new message ID.
175    pub async fn create_draft(
176        &self,
177        account: &str,
178        message: &Outgoing,
179    ) -> Result<String, ClientError> {
180        let token = self.auth.get_valid_token(account).await?;
181        mail::create_draft(&self.http, &self.base_url, &token, message).await
182    }
183
184    /// POST /me/messages/{id}/createReply.
185    pub async fn create_reply_draft(
186        &self,
187        account: &str,
188        message_id: &str,
189        comment: &str,
190    ) -> Result<String, ClientError> {
191        let token = self.auth.get_valid_token(account).await?;
192        mail::create_reply_draft(&self.http, &self.base_url, &token, message_id, comment).await
193    }
194
195    /// POST /me/messages/{id}/createReplyAll.
196    pub async fn create_reply_all_draft(
197        &self,
198        account: &str,
199        message_id: &str,
200        comment: &str,
201    ) -> Result<String, ClientError> {
202        let token = self.auth.get_valid_token(account).await?;
203        mail::create_reply_all_draft(&self.http, &self.base_url, &token, message_id, comment).await
204    }
205
206    /// POST /me/messages/{id}/createForward.
207    pub async fn create_forward_draft(
208        &self,
209        account: &str,
210        message_id: &str,
211        to: &[String],
212        comment: &str,
213    ) -> Result<String, ClientError> {
214        let token = self.auth.get_valid_token(account).await?;
215        mail::create_forward_draft(&self.http, &self.base_url, &token, message_id, to, comment)
216            .await
217    }
218
219    /// POST /me/messages/{id}/send — send an existing draft.
220    pub async fn send_draft(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
221        let token = self.auth.get_valid_token(account).await?;
222        mail::send_draft(&self.http, &self.base_url, &token, message_id).await
223    }
224
225    /// PATCH /me/messages/{id} — overwrite a draft's editable fields.
226    pub async fn update_draft(
227        &self,
228        account: &str,
229        message_id: &str,
230        message: &Outgoing,
231    ) -> Result<(), ClientError> {
232        let token = self.auth.get_valid_token(account).await?;
233        mail::update_draft(&self.http, &self.base_url, &token, message_id, message).await
234    }
235
236    /// DELETE /me/messages/{id} — moves to Deleted Items. Works for both
237    /// drafts and inbox messages.
238    pub async fn delete_message(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
239        let token = self.auth.get_valid_token(account).await?;
240        mail::delete_message(&self.http, &self.base_url, &token, message_id).await
241    }
242
243    /// POST /me/messages/{id}/attachments — attach a file (simple upload).
244    pub async fn add_attachment(
245        &self,
246        account: &str,
247        message_id: &str,
248        name: &str,
249        content_type: &str,
250        bytes: &[u8],
251    ) -> Result<String, ClientError> {
252        let token = self.auth.get_valid_token(account).await?;
253        mail::add_attachment(
254            &self.http,
255            &self.base_url,
256            &token,
257            message_id,
258            name,
259            content_type,
260            bytes,
261        )
262        .await
263    }
264
265    /// DELETE /me/messages/{id}/attachments/{att_id}.
266    pub async fn delete_attachment(
267        &self,
268        account: &str,
269        message_id: &str,
270        attachment_id: &str,
271    ) -> Result<(), ClientError> {
272        let token = self.auth.get_valid_token(account).await?;
273        mail::delete_attachment(
274            &self.http,
275            &self.base_url,
276            &token,
277            message_id,
278            attachment_id,
279        )
280        .await
281    }
282
283    /// GET /me/messages/{id} for a given account email.
284    pub async fn get_message(
285        &self,
286        account: &str,
287        message_id: &str,
288    ) -> Result<pidge_core::FullMessage, ClientError> {
289        let token = self.auth.get_valid_token(account).await?;
290        mail::get_message(&self.http, &self.base_url, &token, account, message_id).await
291    }
292
293    /// GET /me/messages/{id}?$select=internetMessageHeaders.
294    pub async fn fetch_message_headers(
295        &self,
296        account: &str,
297        message_id: &str,
298    ) -> Result<Vec<(String, String)>, ClientError> {
299        let token = self.auth.get_valid_token(account).await?;
300        mail::fetch_message_headers(&self.http, &self.base_url, &token, message_id).await
301    }
302
303    /// GET /me/messages/{id}/attachments.
304    pub async fn list_attachments(
305        &self,
306        account: &str,
307        message_id: &str,
308    ) -> Result<Vec<pidge_core::Attachment>, ClientError> {
309        let token = self.auth.get_valid_token(account).await?;
310        mail::list_attachments(&self.http, &self.base_url, &token, message_id).await
311    }
312
313    /// GET /me/messages/{id}/attachments/{att_id} returning decoded bytes.
314    pub async fn get_attachment_bytes(
315        &self,
316        account: &str,
317        message_id: &str,
318        attachment_id: &str,
319    ) -> Result<Vec<u8>, ClientError> {
320        let token = self.auth.get_valid_token(account).await?;
321        mail::get_attachment_bytes(
322            &self.http,
323            &self.base_url,
324            &token,
325            message_id,
326            attachment_id,
327        )
328        .await
329    }
330
331    /// PATCH /me/messages/{id} with isRead: true.
332    pub async fn mark_read(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
333        let token = self.auth.get_valid_token(account).await?;
334        mail::mark_read(&self.http, &self.base_url, &token, message_id).await
335    }
336
337    // -------- Calendar surface --------
338
339    /// GET /me/calendars.
340    pub async fn list_calendars(
341        &self,
342        account: &str,
343    ) -> Result<Vec<pidge_core::Calendar>, ClientError> {
344        let token = self.auth.get_valid_token(account).await?;
345        calendars::list_calendars(&self.http, &self.base_url, &token, account).await
346    }
347
348    /// GET /me/calendarView (or /me/calendars/{id}/calendarView).
349    pub async fn list_calendar_view(
350        &self,
351        account: &str,
352        calendar_id: Option<&str>,
353        start: chrono::DateTime<chrono::Utc>,
354        end: chrono::DateTime<chrono::Utc>,
355        limit: usize,
356    ) -> Result<events::EventsPage, ClientError> {
357        let token = self.auth.get_valid_token(account).await?;
358        events::list_calendar_view(
359            &self.http,
360            &self.base_url,
361            &token,
362            account,
363            calendar_id,
364            start,
365            end,
366            limit,
367        )
368        .await
369    }
370
371    /// GET /me/events/{id}.
372    pub async fn get_event(
373        &self,
374        account: &str,
375        event_id: &str,
376    ) -> Result<pidge_core::Event, ClientError> {
377        let token = self.auth.get_valid_token(account).await?;
378        events::get_event(&self.http, &self.base_url, &token, account, event_id).await
379    }
380
381    /// POST /me/calendar/events (or /me/calendars/{id}/events).
382    pub async fn create_event(
383        &self,
384        account: &str,
385        calendar_id: Option<&str>,
386        new_event: &events::NewEvent,
387    ) -> Result<String, ClientError> {
388        let token = self.auth.get_valid_token(account).await?;
389        events::create_event(&self.http, &self.base_url, &token, calendar_id, new_event).await
390    }
391
392    /// PATCH /me/events/{id} — overwrite editable fields.
393    pub async fn update_event(
394        &self,
395        account: &str,
396        event_id: &str,
397        new_event: &events::NewEvent,
398    ) -> Result<(), ClientError> {
399        let token = self.auth.get_valid_token(account).await?;
400        events::update_event(&self.http, &self.base_url, &token, event_id, new_event).await
401    }
402
403    /// PATCH /me/events/{id} — change only start + end.
404    pub async fn move_time(
405        &self,
406        account: &str,
407        event_id: &str,
408        start: chrono::DateTime<chrono::Utc>,
409        end: chrono::DateTime<chrono::Utc>,
410        tz: &str,
411    ) -> Result<(), ClientError> {
412        let token = self.auth.get_valid_token(account).await?;
413        events::move_time(&self.http, &self.base_url, &token, event_id, start, end, tz).await
414    }
415
416    /// DELETE /me/events/{id} — silent removal.
417    pub async fn delete_event(&self, account: &str, event_id: &str) -> Result<(), ClientError> {
418        let token = self.auth.get_valid_token(account).await?;
419        events::delete_event(&self.http, &self.base_url, &token, event_id).await
420    }
421
422    /// POST /me/events/{id}/cancel — organizer-only.
423    pub async fn cancel_event(
424        &self,
425        account: &str,
426        event_id: &str,
427        comment: &str,
428    ) -> Result<(), ClientError> {
429        let token = self.auth.get_valid_token(account).await?;
430        events::cancel_event(&self.http, &self.base_url, &token, event_id, comment).await
431    }
432
433    /// POST /me/events/{id}/accept | /tentativelyAccept | /decline.
434    pub async fn rsvp_event(
435        &self,
436        account: &str,
437        event_id: &str,
438        kind: events::RsvpKind,
439        comment: &str,
440        send_response: bool,
441    ) -> Result<(), ClientError> {
442        let token = self.auth.get_valid_token(account).await?;
443        events::rsvp_event(
444            &self.http,
445            &self.base_url,
446            &token,
447            event_id,
448            kind,
449            comment,
450            send_response,
451        )
452        .await
453    }
454
455    /// PATCH /me/events/{id} with `calendar@odata.bind` — move between calendars.
456    pub async fn move_event_to_calendar(
457        &self,
458        account: &str,
459        event_id: &str,
460        destination_calendar_id: &str,
461    ) -> Result<(), ClientError> {
462        let token = self.auth.get_valid_token(account).await?;
463        events::move_event_to_calendar(
464            &self.http,
465            &self.base_url,
466            &token,
467            event_id,
468            destination_calendar_id,
469        )
470        .await
471    }
472
473    /// GET /me/events?$search="<query>".
474    pub async fn search_events(
475        &self,
476        account: &str,
477        query: &str,
478        limit: usize,
479    ) -> Result<Vec<pidge_core::Event>, ClientError> {
480        let token = self.auth.get_valid_token(account).await?;
481        events::search_events(&self.http, &self.base_url, &token, account, query, limit).await
482    }
483}