Skip to main content

io_msgraph/v1/
send.rs

1//! HTTP/JSON transport every Microsoft Graph coroutine delegates to:
2//! builds the authorized request and parses the JSON response, or the
3//! Graph error envelope on failure.
4//!
5//! Microsoft Graph reference:
6//! <https://learn.microsoft.com/en-us/graph/api/overview>.
7
8use core::{error::Error, fmt, marker::PhantomData};
9
10use alloc::{
11    format,
12    string::{String, ToString},
13    vec::Vec,
14};
15
16use io_http::{
17    coroutine::{HttpCoroutine, HttpCoroutineState},
18    rfc6750::bearer::HttpAuthBearer,
19    rfc9110::{
20        request::HttpRequest,
21        send::{HttpSendOutput, HttpSendYield},
22    },
23    rfc9112::send::{Http11Send, Http11SendError},
24};
25use log::{debug, trace};
26use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned};
27use url::Url;
28
29use crate::coroutine::{MsgraphCoroutine, MsgraphCoroutineState, MsgraphYield};
30
31/// Base URL of the Microsoft Graph API, version 1.0.
32pub const MSGRAPH_API_BASE: &str = "https://graph.microsoft.com/v1.0/";
33
34/// Base path segment addressing a mailbox owner: `me` as-is, any
35/// other value as `users/{id}`.
36///
37/// Graph accepts an explicit user id or principal name under `users/`
38/// but rejects `users/me`; only the bare `me` shortcut addresses the
39/// authenticated user.
40pub fn user_path(user_id: &str) -> String {
41    if user_id == "me" {
42        String::from("me")
43    } else {
44        format!("users/{user_id}")
45    }
46}
47
48/// Marker for endpoints that return an empty body (DELETE, sendMail,
49/// send draft); deserialises from anything, including nothing.
50#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
51pub struct MsgraphNoResponse;
52
53impl<'de> Deserialize<'de> for MsgraphNoResponse {
54    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
55    where
56        D: Deserializer<'de>,
57    {
58        let _ = serde::de::IgnoredAny::deserialize(deserializer)?;
59        Ok(Self)
60    }
61}
62
63/// Error returned by [`MsgraphSend`] and the raw `$value` coroutines.
64#[derive(Debug)]
65pub enum MsgraphSendError {
66    /// The underlying HTTP/1.1 exchange failed.
67    Send(Http11SendError),
68    /// The JSON request body could not be serialized.
69    SerializeRequest(serde_json::Error),
70    /// The 2xx response body could not be deserialized.
71    ParseResponse(serde_json::Error),
72    /// The request URL could not be built.
73    ParseUrl(url::ParseError),
74    /// The request arguments were rejected before sending.
75    InvalidRequest(String),
76    /// The Graph API answered a non-2xx status; carries the parsed
77    /// error envelope.
78    Api {
79        /// The HTTP status of the response.
80        status: u16,
81        /// The error code of the Graph error envelope.
82        code: String,
83        /// The human-readable message of the Graph error envelope.
84        message: String,
85    },
86    /// The server answered a 3xx; redirects are never followed.
87    UnexpectedRedirect,
88}
89
90impl fmt::Display for MsgraphSendError {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            Self::Send(err) => write!(f, "Microsoft Graph HTTP request failed: {err}"),
94            Self::SerializeRequest(err) => {
95                write!(f, "Microsoft Graph request serialization failed: {err}")
96            }
97            Self::ParseResponse(err) => write!(f, "Microsoft Graph response parsing failed: {err}"),
98            Self::ParseUrl(err) => write!(f, "Microsoft Graph URL parsing failed: {err}"),
99            Self::InvalidRequest(reason) => write!(f, "Invalid Microsoft Graph request: {reason}"),
100            Self::Api {
101                status,
102                code,
103                message,
104            } => write!(
105                f,
106                "Microsoft Graph API returned HTTP {status} ({code}): {message}"
107            ),
108            Self::UnexpectedRedirect => {
109                write!(f, "Microsoft Graph server returned an unexpected redirect")
110            }
111        }
112    }
113}
114
115impl Error for MsgraphSendError {
116    fn source(&self) -> Option<&(dyn Error + 'static)> {
117        match self {
118            Self::Send(err) => Some(err),
119            Self::SerializeRequest(err) | Self::ParseResponse(err) => Some(err),
120            Self::ParseUrl(err) => Some(err),
121            _ => None,
122        }
123    }
124}
125
126impl From<Http11SendError> for MsgraphSendError {
127    fn from(err: Http11SendError) -> Self {
128        Self::Send(err)
129    }
130}
131
132impl From<url::ParseError> for MsgraphSendError {
133    fn from(err: url::ParseError) -> Self {
134        Self::ParseUrl(err)
135    }
136}
137
138impl MsgraphSendError {
139    /// The HTTP status of an [`Api`](Self::Api) error, `None` for
140    /// every other variant.
141    pub fn status(&self) -> Option<u16> {
142        match self {
143            Self::Api { status, .. } => Some(*status),
144            _ => None,
145        }
146    }
147
148    /// True for API statuses worth retrying (429 and common 5xx).
149    pub fn is_retryable(&self) -> bool {
150        matches!(self.status(), Some(429 | 500 | 502 | 503 | 504))
151    }
152}
153
154/// Terminal value of every coroutine: the parsed response plus the
155/// connection reuse hint.
156#[derive(Clone, Debug)]
157pub struct MsgraphSendOutput<T> {
158    /// The parsed response body.
159    pub response: T,
160    /// Whether the server allows reusing the TCP/TLS connection.
161    pub keep_alive: bool,
162}
163
164/// I/O-free coroutine sending one authorized Microsoft Graph request
165/// and parsing its JSON response into `T`.
166pub struct MsgraphSend<T> {
167    state: State,
168    _phantom: PhantomData<T>,
169}
170
171impl<T: DeserializeOwned> MsgraphSend<T> {
172    /// Builds a GET send for the given URL.
173    pub fn get(auth: &HttpAuthBearer, url: Url) -> Self {
174        Self::with_method(auth, "GET", url, None, Vec::new())
175    }
176
177    /// Builds a DELETE send for the given URL.
178    pub fn delete(auth: &HttpAuthBearer, url: Url) -> Self {
179        Self::with_method(auth, "DELETE", url, None, Vec::new())
180    }
181
182    /// Builds a POST send with a JSON body.
183    pub fn post_json<B: Serialize>(
184        auth: &HttpAuthBearer,
185        url: Url,
186        body: &B,
187    ) -> Result<Self, MsgraphSendError> {
188        let body = serde_json::to_vec(body).map_err(MsgraphSendError::SerializeRequest)?;
189        Ok(Self::with_method(
190            auth,
191            "POST",
192            url,
193            Some("application/json"),
194            body,
195        ))
196    }
197
198    /// Builds a PATCH send with a JSON body.
199    pub fn patch_json<B: Serialize>(
200        auth: &HttpAuthBearer,
201        url: Url,
202        body: &B,
203    ) -> Result<Self, MsgraphSendError> {
204        let body = serde_json::to_vec(body).map_err(MsgraphSendError::SerializeRequest)?;
205        Ok(Self::with_method(
206            auth,
207            "PATCH",
208            url,
209            Some("application/json"),
210            body,
211        ))
212    }
213
214    /// Builds a POST send with a text/plain body.
215    pub fn post_text(auth: &HttpAuthBearer, url: Url, body: Vec<u8>) -> Self {
216        Self::with_method(auth, "POST", url, Some("text/plain"), body)
217    }
218
219    /// Builds a send with an arbitrary method, content type and body.
220    pub fn with_method(
221        auth: &HttpAuthBearer,
222        method: &str,
223        url: Url,
224        content_type: Option<&str>,
225        body: Vec<u8>,
226    ) -> Self {
227        let mut request = HttpRequest::get(url.clone())
228            .header("Accept", "application/json")
229            .header("Authorization", auth.to_authorization())
230            .body(body);
231
232        if let Some(content_type) = content_type {
233            request = request.header("Content-Type", content_type);
234        }
235
236        request.method = method.into();
237
238        debug!("prepare request to send");
239        trace!("method: {method}");
240        trace!("url: {url}");
241
242        Self {
243            state: State::Prepare(request),
244            _phantom: PhantomData,
245        }
246    }
247
248    /// Adds a header to the request.
249    ///
250    /// Only effective before the first
251    /// [`resume`](MsgraphCoroutine::resume), which serializes the
252    /// request: a header added later is ignored.
253    pub fn header(mut self, name: impl ToString, value: impl ToString) -> Self {
254        if let State::Prepare(request) = &mut self.state {
255            request.headers.push((name.to_string(), value.to_string()));
256        }
257        self
258    }
259
260    /// Asks Graph for at most `size` items per page of a paged
261    /// collection (`Prefer: odata.maxpagesize={size}`).
262    ///
263    /// The preference rides on one request only: a caller following
264    /// `@odata.nextLink` asks again on every page. Graph may serve
265    /// fewer items than asked, and caps the size per endpoint.
266    pub fn max_page_size(self, size: u32) -> Self {
267        self.header("Prefer", format!("odata.maxpagesize={size}"))
268    }
269}
270
271impl<T: DeserializeOwned> MsgraphCoroutine for MsgraphSend<T> {
272    type Yield = MsgraphYield;
273    type Return = Result<MsgraphSendOutput<T>, MsgraphSendError>;
274
275    fn resume(&mut self, arg: Option<&[u8]>) -> MsgraphCoroutineState<Self::Yield, Self::Return> {
276        if let State::Prepare(request) = &mut self.state {
277            let url = request.url.clone();
278            let request = core::mem::replace(request, HttpRequest::get(url));
279            self.state = State::Send(Http11Send::new(request));
280        }
281
282        match &mut self.state {
283            State::Prepare(_) => unreachable!("the request is sent on the first resume"),
284            State::Send(send) => match send.resume(arg) {
285                HttpCoroutineState::Yielded(HttpSendYield::WantsRead) => {
286                    MsgraphCoroutineState::Yielded(MsgraphYield::WantsRead)
287                }
288                HttpCoroutineState::Yielded(HttpSendYield::WantsWrite(bytes)) => {
289                    MsgraphCoroutineState::Yielded(MsgraphYield::WantsWrite(bytes))
290                }
291                HttpCoroutineState::Yielded(HttpSendYield::WantsRedirect { .. }) => {
292                    MsgraphCoroutineState::Complete(Err(MsgraphSendError::UnexpectedRedirect))
293                }
294                HttpCoroutineState::Complete(Err(err)) => {
295                    MsgraphCoroutineState::Complete(Err(err.into()))
296                }
297                HttpCoroutineState::Complete(Ok(HttpSendOutput {
298                    response,
299                    keep_alive,
300                    ..
301                })) => {
302                    if response.status.is_success() {
303                        let body = if response.body.is_empty() {
304                            b"null".as_slice()
305                        } else {
306                            response.body.as_slice()
307                        };
308
309                        match serde_json::from_slice::<T>(body) {
310                            Ok(response) => {
311                                MsgraphCoroutineState::Complete(Ok(MsgraphSendOutput {
312                                    response,
313                                    keep_alive,
314                                }))
315                            }
316                            Err(err) => MsgraphCoroutineState::Complete(Err(
317                                MsgraphSendError::ParseResponse(err),
318                            )),
319                        }
320                    } else {
321                        let (status, code, message) =
322                            parse_api_error(*response.status, &response.body);
323                        MsgraphCoroutineState::Complete(Err(MsgraphSendError::Api {
324                            status,
325                            code,
326                            message,
327                        }))
328                    }
329                }
330            },
331        }
332    }
333}
334
335enum State {
336    /// The request, open to more headers until the first resume.
337    Prepare(HttpRequest),
338    Send(Http11Send),
339}
340
341#[derive(Debug, Deserialize)]
342struct ErrorEnvelope {
343    error: ErrorBody,
344}
345
346#[derive(Debug, Deserialize)]
347struct ErrorBody {
348    code: Option<String>,
349    message: Option<String>,
350}
351
352/// Parse a Microsoft Graph error envelope into its
353/// `(http_status, code, message)` triple.
354///
355/// The envelope is `{ "error": { "code", "message" } }`; when the body
356/// is not that JSON shape the raw body text becomes the message, and
357/// empty or missing parts fall back to `unknown` markers.
358pub fn parse_api_error(http_status: u16, body: &[u8]) -> (u16, String, String) {
359    if let Ok(envelope) = serde_json::from_slice::<ErrorEnvelope>(body) {
360        let code = envelope
361            .error
362            .code
363            .filter(|code| !code.trim().is_empty())
364            .unwrap_or_else(|| String::from("unknown"));
365        let message = envelope
366            .error
367            .message
368            .filter(|message| !message.trim().is_empty())
369            .unwrap_or_else(|| String::from("unknown Microsoft Graph API error"));
370        return (http_status, code, message);
371    }
372
373    let message = String::from_utf8_lossy(body).trim().to_string();
374
375    if message.is_empty() {
376        (
377            http_status,
378            String::from("unknown"),
379            String::from("unknown Microsoft Graph API error"),
380        )
381    } else {
382        (http_status, String::from("unknown"), message)
383    }
384}