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