Skip to main content

pidge_client/graph/
mod.rs

1//! Microsoft Graph API client.
2
3mod mail;
4mod me;
5
6pub use mail::{
7    InboxPage, Outgoing, add_attachment, create_draft, create_forward_draft,
8    create_reply_all_draft, create_reply_draft, delete_attachment, delete_message,
9    fetch_message_headers, forward_message, get_attachment_bytes, get_message, list_attachments,
10    list_drafts, list_inbox, mark_read, mark_unread, move_message, reply_all_message,
11    reply_message, search_messages, send_draft, send_mail, set_flag, update_draft,
12};
13pub use me::{Me, get_me};
14
15use crate::auth::AuthClient;
16use crate::auth::config;
17use crate::error::ClientError;
18use pidge_core::Message;
19
20/// Stateful Microsoft Graph client. Holds an AuthClient and a shared HTTP client.
21pub struct GraphClient {
22    auth: AuthClient,
23    http: reqwest::Client,
24    base_url: String,
25}
26
27impl GraphClient {
28    pub fn new(auth: AuthClient) -> Result<Self, ClientError> {
29        Ok(Self {
30            auth,
31            http: reqwest::Client::builder()
32                .user_agent(format!("pidge/{}", env!("CARGO_PKG_VERSION")))
33                .build()?,
34            base_url: config::GRAPH_BASE.to_string(),
35        })
36    }
37
38    pub fn for_test(auth: AuthClient, base_url: impl Into<String>) -> Self {
39        Self {
40            auth,
41            http: reqwest::Client::new(),
42            base_url: base_url.into(),
43        }
44    }
45
46    pub fn auth(&self) -> &AuthClient {
47        &self.auth
48    }
49
50    /// GET /me. Used right after sign-in to learn the user's email.
51    pub async fn me(&self, access_token: &str) -> Result<Me, ClientError> {
52        get_me(&self.http, &self.base_url, access_token).await
53    }
54
55    /// GET /me/mailFolders/inbox/messages for a given account email.
56    /// Acquires/refreshes a token transparently via `AuthClient::get_valid_token`.
57    pub async fn list_inbox(
58        &self,
59        account: &str,
60        limit: usize,
61        skip: usize,
62        unread_only: bool,
63    ) -> Result<InboxPage, ClientError> {
64        let token = self.auth.get_valid_token(account).await?;
65        list_inbox(
66            &self.http,
67            &self.base_url,
68            &token,
69            account,
70            limit,
71            skip,
72            unread_only,
73        )
74        .await
75    }
76
77    /// GET /me/messages with `$search="<query>"` for a given account.
78    pub async fn search_messages(
79        &self,
80        account: &str,
81        query: &str,
82        limit: usize,
83    ) -> Result<Vec<Message>, ClientError> {
84        let token = self.auth.get_valid_token(account).await?;
85        search_messages(&self.http, &self.base_url, &token, account, query, limit).await
86    }
87
88    /// PATCH /me/messages/{id} with `{ "isRead": false }`.
89    pub async fn mark_unread(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
90        let token = self.auth.get_valid_token(account).await?;
91        mail::mark_unread(&self.http, &self.base_url, &token, message_id).await
92    }
93
94    /// PATCH /me/messages/{id} with `{ "flag": { "flagStatus": "flagged"|"notFlagged" } }`.
95    pub async fn set_flag(
96        &self,
97        account: &str,
98        message_id: &str,
99        flagged: bool,
100    ) -> Result<(), ClientError> {
101        let token = self.auth.get_valid_token(account).await?;
102        mail::set_flag(&self.http, &self.base_url, &token, message_id, flagged).await
103    }
104
105    /// POST /me/messages/{id}/move — move to a folder by ID or well-known name.
106    pub async fn move_message(
107        &self,
108        account: &str,
109        message_id: &str,
110        destination: &str,
111    ) -> Result<(), ClientError> {
112        let token = self.auth.get_valid_token(account).await?;
113        mail::move_message(&self.http, &self.base_url, &token, message_id, destination).await
114    }
115
116    /// POST /me/sendMail — compose-and-send a new message.
117    pub async fn send_mail(&self, account: &str, message: &Outgoing) -> Result<(), ClientError> {
118        let token = self.auth.get_valid_token(account).await?;
119        mail::send_mail(&self.http, &self.base_url, &token, message).await
120    }
121
122    /// POST /me/messages/{id}/reply.
123    pub async fn reply_message(
124        &self,
125        account: &str,
126        message_id: &str,
127        comment: &str,
128    ) -> Result<(), ClientError> {
129        let token = self.auth.get_valid_token(account).await?;
130        mail::reply_message(&self.http, &self.base_url, &token, message_id, comment).await
131    }
132
133    /// POST /me/messages/{id}/replyAll.
134    pub async fn reply_all_message(
135        &self,
136        account: &str,
137        message_id: &str,
138        comment: &str,
139    ) -> Result<(), ClientError> {
140        let token = self.auth.get_valid_token(account).await?;
141        mail::reply_all_message(&self.http, &self.base_url, &token, message_id, comment).await
142    }
143
144    /// POST /me/messages/{id}/forward.
145    pub async fn forward_message(
146        &self,
147        account: &str,
148        message_id: &str,
149        to: &[String],
150        comment: &str,
151    ) -> Result<(), ClientError> {
152        let token = self.auth.get_valid_token(account).await?;
153        mail::forward_message(&self.http, &self.base_url, &token, message_id, to, comment).await
154    }
155
156    /// GET /me/mailFolders/drafts/messages.
157    pub async fn list_drafts(
158        &self,
159        account: &str,
160        limit: usize,
161        skip: usize,
162    ) -> Result<InboxPage, ClientError> {
163        let token = self.auth.get_valid_token(account).await?;
164        mail::list_drafts(&self.http, &self.base_url, &token, account, limit, skip).await
165    }
166
167    /// POST /me/messages — create a draft, returning its new message ID.
168    pub async fn create_draft(
169        &self,
170        account: &str,
171        message: &Outgoing,
172    ) -> Result<String, ClientError> {
173        let token = self.auth.get_valid_token(account).await?;
174        mail::create_draft(&self.http, &self.base_url, &token, message).await
175    }
176
177    /// POST /me/messages/{id}/createReply.
178    pub async fn create_reply_draft(
179        &self,
180        account: &str,
181        message_id: &str,
182        comment: &str,
183    ) -> Result<String, ClientError> {
184        let token = self.auth.get_valid_token(account).await?;
185        mail::create_reply_draft(&self.http, &self.base_url, &token, message_id, comment).await
186    }
187
188    /// POST /me/messages/{id}/createReplyAll.
189    pub async fn create_reply_all_draft(
190        &self,
191        account: &str,
192        message_id: &str,
193        comment: &str,
194    ) -> Result<String, ClientError> {
195        let token = self.auth.get_valid_token(account).await?;
196        mail::create_reply_all_draft(&self.http, &self.base_url, &token, message_id, comment).await
197    }
198
199    /// POST /me/messages/{id}/createForward.
200    pub async fn create_forward_draft(
201        &self,
202        account: &str,
203        message_id: &str,
204        to: &[String],
205        comment: &str,
206    ) -> Result<String, ClientError> {
207        let token = self.auth.get_valid_token(account).await?;
208        mail::create_forward_draft(&self.http, &self.base_url, &token, message_id, to, comment)
209            .await
210    }
211
212    /// POST /me/messages/{id}/send — send an existing draft.
213    pub async fn send_draft(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
214        let token = self.auth.get_valid_token(account).await?;
215        mail::send_draft(&self.http, &self.base_url, &token, message_id).await
216    }
217
218    /// PATCH /me/messages/{id} — overwrite a draft's editable fields.
219    pub async fn update_draft(
220        &self,
221        account: &str,
222        message_id: &str,
223        message: &Outgoing,
224    ) -> Result<(), ClientError> {
225        let token = self.auth.get_valid_token(account).await?;
226        mail::update_draft(&self.http, &self.base_url, &token, message_id, message).await
227    }
228
229    /// DELETE /me/messages/{id} — moves to Deleted Items. Works for both
230    /// drafts and inbox messages.
231    pub async fn delete_message(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
232        let token = self.auth.get_valid_token(account).await?;
233        mail::delete_message(&self.http, &self.base_url, &token, message_id).await
234    }
235
236    /// POST /me/messages/{id}/attachments — attach a file (simple upload).
237    pub async fn add_attachment(
238        &self,
239        account: &str,
240        message_id: &str,
241        name: &str,
242        content_type: &str,
243        bytes: &[u8],
244    ) -> Result<String, ClientError> {
245        let token = self.auth.get_valid_token(account).await?;
246        mail::add_attachment(
247            &self.http,
248            &self.base_url,
249            &token,
250            message_id,
251            name,
252            content_type,
253            bytes,
254        )
255        .await
256    }
257
258    /// DELETE /me/messages/{id}/attachments/{att_id}.
259    pub async fn delete_attachment(
260        &self,
261        account: &str,
262        message_id: &str,
263        attachment_id: &str,
264    ) -> Result<(), ClientError> {
265        let token = self.auth.get_valid_token(account).await?;
266        mail::delete_attachment(
267            &self.http,
268            &self.base_url,
269            &token,
270            message_id,
271            attachment_id,
272        )
273        .await
274    }
275
276    /// GET /me/messages/{id} for a given account email.
277    pub async fn get_message(
278        &self,
279        account: &str,
280        message_id: &str,
281    ) -> Result<pidge_core::FullMessage, ClientError> {
282        let token = self.auth.get_valid_token(account).await?;
283        mail::get_message(&self.http, &self.base_url, &token, account, message_id).await
284    }
285
286    /// GET /me/messages/{id}?$select=internetMessageHeaders.
287    pub async fn fetch_message_headers(
288        &self,
289        account: &str,
290        message_id: &str,
291    ) -> Result<Vec<(String, String)>, ClientError> {
292        let token = self.auth.get_valid_token(account).await?;
293        mail::fetch_message_headers(&self.http, &self.base_url, &token, message_id).await
294    }
295
296    /// GET /me/messages/{id}/attachments.
297    pub async fn list_attachments(
298        &self,
299        account: &str,
300        message_id: &str,
301    ) -> Result<Vec<pidge_core::Attachment>, ClientError> {
302        let token = self.auth.get_valid_token(account).await?;
303        mail::list_attachments(&self.http, &self.base_url, &token, message_id).await
304    }
305
306    /// GET /me/messages/{id}/attachments/{att_id} returning decoded bytes.
307    pub async fn get_attachment_bytes(
308        &self,
309        account: &str,
310        message_id: &str,
311        attachment_id: &str,
312    ) -> Result<Vec<u8>, ClientError> {
313        let token = self.auth.get_valid_token(account).await?;
314        mail::get_attachment_bytes(
315            &self.http,
316            &self.base_url,
317            &token,
318            message_id,
319            attachment_id,
320        )
321        .await
322    }
323
324    /// PATCH /me/messages/{id} with isRead: true.
325    pub async fn mark_read(&self, account: &str, message_id: &str) -> Result<(), ClientError> {
326        let token = self.auth.get_valid_token(account).await?;
327        mail::mark_read(&self.http, &self.base_url, &token, message_id).await
328    }
329}