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