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_categories, get_message, list_attachments, list_child_folders,
18    list_drafts, list_folder_messages, list_inbox, list_mail_folders, mark_read, mark_unread,
19    move_message, reply_all_message, reply_message, search_messages, send_draft, send_mail,
20    set_categories, set_flag, 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    /// GET /me/messages/{id}?$select=categories.
138    pub async fn get_categories(
139        &self,
140        account: &str,
141        message_id: &str,
142    ) -> Result<Vec<String>, ClientError> {
143        let token = self.auth.get_valid_token(account).await?;
144        mail::get_categories(&self.http, &self.base_url, &token, message_id).await
145    }
146
147    /// PATCH /me/messages/{id} categories.
148    pub async fn set_categories(
149        &self,
150        account: &str,
151        message_id: &str,
152        categories: &[String],
153    ) -> Result<(), ClientError> {
154        let token = self.auth.get_valid_token(account).await?;
155        mail::set_categories(&self.http, &self.base_url, &token, message_id, categories).await
156    }
157
158    /// POST /me/messages/{id}/move — move to a folder by ID or well-known name.
159    pub async fn move_message(
160        &self,
161        account: &str,
162        message_id: &str,
163        destination: &str,
164    ) -> Result<(), ClientError> {
165        let token = self.auth.get_valid_token(account).await?;
166        mail::move_message(&self.http, &self.base_url, &token, message_id, destination).await
167    }
168
169    /// GET /me/mailFolders — list the account's top-level folders.
170    pub async fn list_mail_folders(&self, account: &str) -> Result<Vec<MailFolder>, ClientError> {
171        let token = self.auth.get_valid_token(account).await?;
172        mail::list_mail_folders(&self.http, &self.base_url, &token).await
173    }
174
175    /// POST /me/mailFolders — create a top-level folder, returning it.
176    pub async fn create_mail_folder(
177        &self,
178        account: &str,
179        display_name: &str,
180    ) -> Result<MailFolder, ClientError> {
181        let token = self.auth.get_valid_token(account).await?;
182        mail::create_mail_folder(&self.http, &self.base_url, &token, display_name).await
183    }
184
185    /// GET /me/mailFolders/{parent_id}/childFolders — list a folder's children.
186    pub async fn list_child_folders(
187        &self,
188        account: &str,
189        parent_id: &str,
190    ) -> Result<Vec<MailFolder>, ClientError> {
191        let token = self.auth.get_valid_token(account).await?;
192        mail::list_child_folders(&self.http, &self.base_url, &token, parent_id).await
193    }
194
195    /// POST /me/mailFolders/{parent_id}/childFolders — create a child folder.
196    pub async fn create_child_folder(
197        &self,
198        account: &str,
199        parent_id: &str,
200        display_name: &str,
201    ) -> Result<MailFolder, ClientError> {
202        let token = self.auth.get_valid_token(account).await?;
203        mail::create_child_folder(&self.http, &self.base_url, &token, parent_id, display_name).await
204    }
205
206    /// DELETE /me/mailFolders/{id} — delete a folder (contents move to
207    /// Deleted Items).
208    pub async fn delete_mail_folder(
209        &self,
210        account: &str,
211        folder_id: &str,
212    ) -> Result<(), ClientError> {
213        let token = self.auth.get_valid_token(account).await?;
214        mail::delete_mail_folder(&self.http, &self.base_url, &token, folder_id).await
215    }
216
217    /// POST /me/sendMail — compose-and-send a new message.
218    pub async fn send_mail(&self, account: &str, message: &Outgoing) -> Result<(), ClientError> {
219        let token = self.auth.get_valid_token(account).await?;
220        mail::send_mail(&self.http, &self.base_url, &token, message).await
221    }
222
223    /// POST /me/messages/{id}/reply.
224    pub async fn reply_message(
225        &self,
226        account: &str,
227        message_id: &str,
228        comment: &str,
229    ) -> Result<(), ClientError> {
230        let token = self.auth.get_valid_token(account).await?;
231        mail::reply_message(&self.http, &self.base_url, &token, message_id, comment).await
232    }
233
234    /// POST /me/messages/{id}/replyAll.
235    pub async fn reply_all_message(
236        &self,
237        account: &str,
238        message_id: &str,
239        comment: &str,
240    ) -> Result<(), ClientError> {
241        let token = self.auth.get_valid_token(account).await?;
242        mail::reply_all_message(&self.http, &self.base_url, &token, message_id, comment).await
243    }
244
245    /// POST /me/messages/{id}/forward.
246    pub async fn forward_message(
247        &self,
248        account: &str,
249        message_id: &str,
250        to: &[String],
251        comment: &str,
252    ) -> Result<(), ClientError> {
253        let token = self.auth.get_valid_token(account).await?;
254        mail::forward_message(&self.http, &self.base_url, &token, message_id, to, comment).await
255    }
256
257    /// GET /me/mailFolders/drafts/messages.
258    pub async fn list_drafts(
259        &self,
260        account: &str,
261        limit: usize,
262        skip: usize,
263    ) -> Result<InboxPage, ClientError> {
264        let token = self.auth.get_valid_token(account).await?;
265        mail::list_drafts(&self.http, &self.base_url, &token, account, limit, skip).await
266    }
267
268    /// POST /me/messages — create a draft, returning its new message ID.
269    pub async fn create_draft(
270        &self,
271        account: &str,
272        message: &Outgoing,
273    ) -> Result<String, ClientError> {
274        let token = self.auth.get_valid_token(account).await?;
275        mail::create_draft(&self.http, &self.base_url, &token, message).await
276    }
277
278    /// POST /me/messages/{id}/createReply.
279    pub async fn create_reply_draft(
280        &self,
281        account: &str,
282        message_id: &str,
283        comment: &str,
284    ) -> Result<String, ClientError> {
285        let token = self.auth.get_valid_token(account).await?;
286        mail::create_reply_draft(&self.http, &self.base_url, &token, message_id, comment).await
287    }
288
289    /// POST /me/messages/{id}/createReplyAll.
290    pub async fn create_reply_all_draft(
291        &self,
292        account: &str,
293        message_id: &str,
294        comment: &str,
295    ) -> Result<String, ClientError> {
296        let token = self.auth.get_valid_token(account).await?;
297        mail::create_reply_all_draft(&self.http, &self.base_url, &token, message_id, comment).await
298    }
299
300    /// POST /me/messages/{id}/createForward.
301    pub async fn create_forward_draft(
302        &self,
303        account: &str,
304        message_id: &str,
305        to: &[String],
306        comment: &str,
307    ) -> Result<String, ClientError> {
308        let token = self.auth.get_valid_token(account).await?;
309        mail::create_forward_draft(&self.http, &self.base_url, &token, message_id, to, comment)
310            .await
311    }
312
313    /// POST /me/messages/{id}/send — send an existing draft.
314    pub async fn send_draft(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
315        let token = self.auth.get_valid_token(account).await?;
316        mail::send_draft(&self.http, &self.base_url, &token, message_id).await
317    }
318
319    /// PATCH /me/messages/{id} — overwrite a draft's editable fields.
320    pub async fn update_draft(
321        &self,
322        account: &str,
323        message_id: &str,
324        message: &Outgoing,
325    ) -> Result<(), ClientError> {
326        let token = self.auth.get_valid_token(account).await?;
327        mail::update_draft(&self.http, &self.base_url, &token, message_id, message).await
328    }
329
330    /// DELETE /me/messages/{id} — moves to Deleted Items. Works for both
331    /// drafts and inbox messages.
332    pub async fn delete_message(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
333        let token = self.auth.get_valid_token(account).await?;
334        mail::delete_message(&self.http, &self.base_url, &token, message_id).await
335    }
336
337    /// POST /me/messages/{id}/attachments — attach a file (simple upload).
338    pub async fn add_attachment(
339        &self,
340        account: &str,
341        message_id: &str,
342        name: &str,
343        content_type: &str,
344        bytes: &[u8],
345    ) -> Result<String, ClientError> {
346        let token = self.auth.get_valid_token(account).await?;
347        mail::add_attachment(
348            &self.http,
349            &self.base_url,
350            &token,
351            message_id,
352            name,
353            content_type,
354            bytes,
355        )
356        .await
357    }
358
359    /// DELETE /me/messages/{id}/attachments/{att_id}.
360    pub async fn delete_attachment(
361        &self,
362        account: &str,
363        message_id: &str,
364        attachment_id: &str,
365    ) -> Result<(), ClientError> {
366        let token = self.auth.get_valid_token(account).await?;
367        mail::delete_attachment(
368            &self.http,
369            &self.base_url,
370            &token,
371            message_id,
372            attachment_id,
373        )
374        .await
375    }
376
377    /// GET /me/messages/{id} for a given account email.
378    pub async fn get_message(
379        &self,
380        account: &str,
381        message_id: &str,
382    ) -> Result<pidge_core::FullMessage, ClientError> {
383        let token = self.auth.get_valid_token(account).await?;
384        mail::get_message(&self.http, &self.base_url, &token, account, message_id).await
385    }
386
387    /// GET /me/messages/{id}?$select=internetMessageHeaders.
388    pub async fn fetch_message_headers(
389        &self,
390        account: &str,
391        message_id: &str,
392    ) -> Result<Vec<(String, String)>, ClientError> {
393        let token = self.auth.get_valid_token(account).await?;
394        mail::fetch_message_headers(&self.http, &self.base_url, &token, message_id).await
395    }
396
397    /// GET /me/messages/{id}/attachments.
398    pub async fn list_attachments(
399        &self,
400        account: &str,
401        message_id: &str,
402    ) -> Result<Vec<pidge_core::Attachment>, ClientError> {
403        let token = self.auth.get_valid_token(account).await?;
404        mail::list_attachments(&self.http, &self.base_url, &token, message_id).await
405    }
406
407    /// GET /me/messages/{id}/attachments/{att_id} returning decoded bytes.
408    pub async fn get_attachment_bytes(
409        &self,
410        account: &str,
411        message_id: &str,
412        attachment_id: &str,
413    ) -> Result<Vec<u8>, ClientError> {
414        let token = self.auth.get_valid_token(account).await?;
415        mail::get_attachment_bytes(
416            &self.http,
417            &self.base_url,
418            &token,
419            message_id,
420            attachment_id,
421        )
422        .await
423    }
424
425    /// PATCH /me/messages/{id} with isRead: true.
426    pub async fn mark_read(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
427        let token = self.auth.get_valid_token(account).await?;
428        mail::mark_read(&self.http, &self.base_url, &token, message_id).await
429    }
430
431    // -------- Calendar surface --------
432
433    /// GET /me/calendars.
434    pub async fn list_calendars(
435        &self,
436        account: &str,
437    ) -> Result<Vec<pidge_core::Calendar>, ClientError> {
438        let token = self.auth.get_valid_token(account).await?;
439        calendars::list_calendars(&self.http, &self.base_url, &token, account).await
440    }
441
442    /// GET /me/calendarView (or /me/calendars/{id}/calendarView).
443    pub async fn list_calendar_view(
444        &self,
445        account: &str,
446        calendar_id: Option<&str>,
447        start: chrono::DateTime<chrono::Utc>,
448        end: chrono::DateTime<chrono::Utc>,
449        limit: usize,
450    ) -> Result<events::EventsPage, ClientError> {
451        let token = self.auth.get_valid_token(account).await?;
452        events::list_calendar_view(
453            &self.http,
454            &self.base_url,
455            &token,
456            account,
457            calendar_id,
458            start,
459            end,
460            limit,
461        )
462        .await
463    }
464
465    /// GET /me/events/{id}.
466    pub async fn get_event(
467        &self,
468        account: &str,
469        event_id: &str,
470    ) -> Result<pidge_core::Event, ClientError> {
471        let token = self.auth.get_valid_token(account).await?;
472        events::get_event(&self.http, &self.base_url, &token, account, event_id).await
473    }
474
475    /// POST /me/calendar/events (or /me/calendars/{id}/events).
476    pub async fn create_event(
477        &self,
478        account: &str,
479        calendar_id: Option<&str>,
480        new_event: &events::NewEvent,
481    ) -> Result<String, ClientError> {
482        let token = self.auth.get_valid_token(account).await?;
483        events::create_event(&self.http, &self.base_url, &token, calendar_id, new_event).await
484    }
485
486    /// PATCH /me/events/{id} — overwrite editable fields.
487    pub async fn update_event(
488        &self,
489        account: &str,
490        event_id: &str,
491        new_event: &events::NewEvent,
492    ) -> Result<(), ClientError> {
493        let token = self.auth.get_valid_token(account).await?;
494        events::update_event(&self.http, &self.base_url, &token, event_id, new_event).await
495    }
496
497    /// PATCH /me/events/{id} — change only start + end.
498    pub async fn move_time(
499        &self,
500        account: &str,
501        event_id: &str,
502        start: chrono::DateTime<chrono::Utc>,
503        end: chrono::DateTime<chrono::Utc>,
504        tz: &str,
505    ) -> Result<(), ClientError> {
506        let token = self.auth.get_valid_token(account).await?;
507        events::move_time(&self.http, &self.base_url, &token, event_id, start, end, tz).await
508    }
509
510    /// DELETE /me/events/{id} — silent removal.
511    pub async fn delete_event(&self, account: &str, event_id: &str) -> Result<(), ClientError> {
512        let token = self.auth.get_valid_token(account).await?;
513        events::delete_event(&self.http, &self.base_url, &token, event_id).await
514    }
515
516    /// POST /me/events/{id}/cancel — organizer-only.
517    pub async fn cancel_event(
518        &self,
519        account: &str,
520        event_id: &str,
521        comment: &str,
522    ) -> Result<(), ClientError> {
523        let token = self.auth.get_valid_token(account).await?;
524        events::cancel_event(&self.http, &self.base_url, &token, event_id, comment).await
525    }
526
527    /// POST /me/events/{id}/accept | /tentativelyAccept | /decline.
528    pub async fn rsvp_event(
529        &self,
530        account: &str,
531        event_id: &str,
532        kind: events::RsvpKind,
533        comment: &str,
534        send_response: bool,
535    ) -> Result<(), ClientError> {
536        let token = self.auth.get_valid_token(account).await?;
537        events::rsvp_event(
538            &self.http,
539            &self.base_url,
540            &token,
541            event_id,
542            kind,
543            comment,
544            send_response,
545        )
546        .await
547    }
548
549    /// PATCH /me/events/{id} with `calendar@odata.bind` — move between calendars.
550    pub async fn move_event_to_calendar(
551        &self,
552        account: &str,
553        event_id: &str,
554        destination_calendar_id: &str,
555    ) -> Result<(), ClientError> {
556        let token = self.auth.get_valid_token(account).await?;
557        events::move_event_to_calendar(
558            &self.http,
559            &self.base_url,
560            &token,
561            event_id,
562            destination_calendar_id,
563        )
564        .await
565    }
566}