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::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 thiserror::Error;
28use url::Url;
29
30use crate::coroutine::{MsgraphCoroutine, MsgraphCoroutineState, MsgraphYield};
31
32/// Base URL of the Microsoft Graph API, version 1.0.
33pub const MSGRAPH_API_BASE: &str = "https://graph.microsoft.com/v1.0/";
34
35/// Base path segment addressing a mailbox owner: `me` as-is, any
36/// other value as `users/{id}`.
37///
38/// Graph accepts an explicit user id or principal name under `users/`
39/// but rejects `users/me`; only the bare `me` shortcut addresses the
40/// authenticated user.
41pub fn user_path(user_id: &str) -> String {
42    if user_id == "me" {
43        String::from("me")
44    } else {
45        format!("users/{user_id}")
46    }
47}
48
49/// Marker for endpoints that return an empty body (DELETE, sendMail,
50/// send draft); deserialises from anything, including nothing.
51#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
52pub struct MsgraphNoResponse;
53
54impl<'de> Deserialize<'de> for MsgraphNoResponse {
55    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56    where
57        D: Deserializer<'de>,
58    {
59        let _ = serde::de::IgnoredAny::deserialize(deserializer)?;
60        Ok(Self)
61    }
62}
63
64/// Error returned by [`MsgraphSend`] and the raw `$value` coroutines.
65#[derive(Debug, Error)]
66pub enum MsgraphSendError {
67    /// The underlying HTTP/1.1 exchange failed.
68    #[error("Microsoft Graph HTTP request failed: {0}")]
69    Send(#[from] Http11SendError),
70    /// The JSON request body could not be serialized.
71    #[error("Microsoft Graph request serialization failed: {0}")]
72    SerializeRequest(#[source] serde_json::Error),
73    /// The 2xx response body could not be deserialized.
74    #[error("Microsoft Graph response parsing failed: {0}")]
75    ParseResponse(#[source] serde_json::Error),
76    /// The request URL could not be built.
77    #[error("Microsoft Graph URL parsing failed: {0}")]
78    ParseUrl(#[from] url::ParseError),
79    /// The request arguments were rejected before sending.
80    #[error("Invalid Microsoft Graph request: {0}")]
81    InvalidRequest(String),
82    /// The Graph API answered a non-2xx status; carries the parsed
83    /// error envelope.
84    #[error("Microsoft Graph API returned HTTP {status} ({code}): {message}")]
85    Api {
86        /// The HTTP status of the response.
87        status: u16,
88        /// The error code of the Graph error envelope.
89        code: String,
90        /// The human-readable message of the Graph error envelope.
91        message: String,
92    },
93    /// The server answered a 3xx; redirects are never followed.
94    #[error("Microsoft Graph server returned an unexpected redirect")]
95    UnexpectedRedirect,
96}
97
98impl MsgraphSendError {
99    /// The HTTP status of an [`Api`](Self::Api) error, `None` for
100    /// every other variant.
101    pub fn status(&self) -> Option<u16> {
102        match self {
103            Self::Api { status, .. } => Some(*status),
104            _ => None,
105        }
106    }
107
108    /// True for API statuses worth retrying (429 and common 5xx).
109    pub fn is_retryable(&self) -> bool {
110        matches!(self.status(), Some(429 | 500 | 502 | 503 | 504))
111    }
112}
113
114/// Terminal value of every coroutine: the parsed response plus the
115/// connection reuse hint.
116#[derive(Clone, Debug)]
117pub struct MsgraphSendOutput<T> {
118    /// The parsed response body.
119    pub response: T,
120    /// Whether the server allows reusing the TCP/TLS connection.
121    pub keep_alive: bool,
122}
123
124/// I/O-free coroutine sending one authorized Microsoft Graph request
125/// and parsing its JSON response into `T`.
126pub struct MsgraphSend<T> {
127    state: State,
128    _phantom: PhantomData<T>,
129}
130
131impl<T: DeserializeOwned> MsgraphSend<T> {
132    /// Builds a GET send for the given URL.
133    pub fn get(auth: &HttpAuthBearer, url: Url) -> Self {
134        Self::with_method(auth, "GET", url, None, Vec::new())
135    }
136
137    /// Builds a DELETE send for the given URL.
138    pub fn delete(auth: &HttpAuthBearer, url: Url) -> Self {
139        Self::with_method(auth, "DELETE", url, None, Vec::new())
140    }
141
142    /// Builds a POST send with a JSON body.
143    pub fn post_json<B: Serialize>(
144        auth: &HttpAuthBearer,
145        url: Url,
146        body: &B,
147    ) -> Result<Self, MsgraphSendError> {
148        let body = serde_json::to_vec(body).map_err(MsgraphSendError::SerializeRequest)?;
149        Ok(Self::with_method(
150            auth,
151            "POST",
152            url,
153            Some("application/json"),
154            body,
155        ))
156    }
157
158    /// Builds a PATCH send with a JSON body.
159    pub fn patch_json<B: Serialize>(
160        auth: &HttpAuthBearer,
161        url: Url,
162        body: &B,
163    ) -> Result<Self, MsgraphSendError> {
164        let body = serde_json::to_vec(body).map_err(MsgraphSendError::SerializeRequest)?;
165        Ok(Self::with_method(
166            auth,
167            "PATCH",
168            url,
169            Some("application/json"),
170            body,
171        ))
172    }
173
174    /// Builds a POST send with a text/plain body.
175    pub fn post_text(auth: &HttpAuthBearer, url: Url, body: Vec<u8>) -> Self {
176        Self::with_method(auth, "POST", url, Some("text/plain"), body)
177    }
178
179    /// Builds a send with an arbitrary method, content type and body.
180    pub fn with_method(
181        auth: &HttpAuthBearer,
182        method: &str,
183        url: Url,
184        content_type: Option<&str>,
185        body: Vec<u8>,
186    ) -> Self {
187        let mut request = HttpRequest::get(url.clone())
188            .header("Accept", "application/json")
189            .header("Authorization", auth.to_authorization())
190            .body(body);
191
192        if let Some(content_type) = content_type {
193            request = request.header("Content-Type", content_type);
194        }
195
196        request.method = method.into();
197
198        debug!("prepare request to send");
199        trace!("method: {method}");
200        trace!("url: {url}");
201
202        Self {
203            state: State::Send(Http11Send::new(request)),
204            _phantom: PhantomData,
205        }
206    }
207}
208
209impl<T: DeserializeOwned> MsgraphCoroutine for MsgraphSend<T> {
210    type Yield = MsgraphYield;
211    type Return = Result<MsgraphSendOutput<T>, MsgraphSendError>;
212
213    fn resume(&mut self, arg: Option<&[u8]>) -> MsgraphCoroutineState<Self::Yield, Self::Return> {
214        match &mut self.state {
215            State::Send(send) => match send.resume(arg) {
216                HttpCoroutineState::Yielded(HttpSendYield::WantsRead) => {
217                    MsgraphCoroutineState::Yielded(MsgraphYield::WantsRead)
218                }
219                HttpCoroutineState::Yielded(HttpSendYield::WantsWrite(bytes)) => {
220                    MsgraphCoroutineState::Yielded(MsgraphYield::WantsWrite(bytes))
221                }
222                HttpCoroutineState::Yielded(HttpSendYield::WantsRedirect { .. }) => {
223                    MsgraphCoroutineState::Complete(Err(MsgraphSendError::UnexpectedRedirect))
224                }
225                HttpCoroutineState::Complete(Err(err)) => {
226                    MsgraphCoroutineState::Complete(Err(err.into()))
227                }
228                HttpCoroutineState::Complete(Ok(HttpSendOutput {
229                    response,
230                    keep_alive,
231                    ..
232                })) => {
233                    if response.status.is_success() {
234                        let body = if response.body.is_empty() {
235                            b"null".as_slice()
236                        } else {
237                            response.body.as_slice()
238                        };
239
240                        match serde_json::from_slice::<T>(body) {
241                            Ok(response) => {
242                                MsgraphCoroutineState::Complete(Ok(MsgraphSendOutput {
243                                    response,
244                                    keep_alive,
245                                }))
246                            }
247                            Err(err) => MsgraphCoroutineState::Complete(Err(
248                                MsgraphSendError::ParseResponse(err),
249                            )),
250                        }
251                    } else {
252                        let (status, code, message) =
253                            parse_api_error(*response.status, &response.body);
254                        MsgraphCoroutineState::Complete(Err(MsgraphSendError::Api {
255                            status,
256                            code,
257                            message,
258                        }))
259                    }
260                }
261            },
262        }
263    }
264}
265
266enum State {
267    Send(Http11Send),
268}
269
270#[derive(Debug, Deserialize)]
271struct ErrorEnvelope {
272    error: ErrorBody,
273}
274
275#[derive(Debug, Deserialize)]
276struct ErrorBody {
277    code: Option<String>,
278    message: Option<String>,
279}
280
281/// Parse a Microsoft Graph error envelope into its
282/// `(http_status, code, message)` triple.
283///
284/// The envelope is `{ "error": { "code", "message" } }`; when the body
285/// is not that JSON shape the raw body text becomes the message, and
286/// empty or missing parts fall back to `unknown` markers.
287pub fn parse_api_error(http_status: u16, body: &[u8]) -> (u16, String, String) {
288    if let Ok(envelope) = serde_json::from_slice::<ErrorEnvelope>(body) {
289        let code = envelope
290            .error
291            .code
292            .filter(|code| !code.trim().is_empty())
293            .unwrap_or_else(|| String::from("unknown"));
294        let message = envelope
295            .error
296            .message
297            .filter(|message| !message.trim().is_empty())
298            .unwrap_or_else(|| String::from("unknown Microsoft Graph API error"));
299        return (http_status, code, message);
300    }
301
302    let message = String::from_utf8_lossy(body).trim().to_string();
303
304    if message.is_empty() {
305        (
306            http_status,
307            String::from("unknown"),
308            String::from("unknown Microsoft Graph API error"),
309        )
310    } else {
311        (http_status, String::from("unknown"), message)
312    }
313}