1use core::marker::PhantomData;
9
10use alloc::{
11 string::{String, ToString},
12 vec::Vec,
13};
14
15use io_http::{
16 coroutine::*,
17 rfc6750::bearer::HttpAuthBearer,
18 rfc9110::{
19 request::HttpRequest,
20 send::{HttpSendOutput, HttpSendYield},
21 },
22 rfc9112::send::{Http11Send, Http11SendError},
23};
24use log::{debug, trace};
25use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned};
26use thiserror::Error;
27use url::Url;
28
29use crate::coroutine::*;
30
31pub const GMAIL_API_BASE: &str = "https://gmail.googleapis.com/gmail/v1/";
33
34#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
37pub struct GmailNoResponse;
38
39impl<'de> Deserialize<'de> for GmailNoResponse {
40 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41 where
42 D: Deserializer<'de>,
43 {
44 let _ = serde::de::IgnoredAny::deserialize(deserializer)?;
45 Ok(Self)
46 }
47}
48
49#[derive(Debug, Error)]
51pub enum GmailSendError {
52 #[error("Gmail HTTP request failed: {0}")]
54 Send(#[from] Http11SendError),
55 #[error("Gmail request serialization failed: {0}")]
57 SerializeRequest(#[source] serde_json::Error),
58 #[error("Gmail response parsing failed: {0}")]
60 ParseResponse(#[source] serde_json::Error),
61 #[error("Gmail URL parsing failed: {0}")]
63 ParseUrl(#[from] url::ParseError),
64 #[error("Invalid Gmail request: {0}")]
66 InvalidRequest(String),
67 #[error("Gmail API returned HTTP {status}: {message}")]
69 Api {
70 status: u16,
72 message: String,
74 },
75 #[error("Gmail server returned an unexpected redirect")]
77 UnexpectedRedirect,
78}
79
80impl GmailSendError {
81 pub fn status(&self) -> Option<u16> {
83 match self {
84 Self::Api { status, .. } => Some(*status),
85 _ => None,
86 }
87 }
88
89 pub fn is_retryable(&self) -> bool {
91 matches!(self.status(), Some(429 | 500 | 502 | 503 | 504))
92 }
93}
94
95#[derive(Clone, Debug)]
97pub struct GmailSendOutput<T> {
98 pub response: T,
100 pub keep_alive: bool,
102}
103
104pub struct GmailSend<T> {
107 state: State,
108 _phantom: PhantomData<T>,
109}
110
111impl<T: DeserializeOwned> GmailSend<T> {
112 pub fn get(auth: &HttpAuthBearer, url: Url) -> Self {
114 Self::with_method(auth, "GET", url, None, Vec::new())
115 }
116
117 pub fn delete(auth: &HttpAuthBearer, url: Url) -> Self {
119 Self::with_method(auth, "DELETE", url, None, Vec::new())
120 }
121
122 pub fn post_json<B: Serialize>(
124 auth: &HttpAuthBearer,
125 url: Url,
126 body: &B,
127 ) -> Result<Self, GmailSendError> {
128 let body = serde_json::to_vec(body).map_err(GmailSendError::SerializeRequest)?;
129 Ok(Self::with_method(
130 auth,
131 "POST",
132 url,
133 Some("application/json"),
134 body,
135 ))
136 }
137
138 pub fn put_json<B: Serialize>(
140 auth: &HttpAuthBearer,
141 url: Url,
142 body: &B,
143 ) -> Result<Self, GmailSendError> {
144 let body = serde_json::to_vec(body).map_err(GmailSendError::SerializeRequest)?;
145 Ok(Self::with_method(
146 auth,
147 "PUT",
148 url,
149 Some("application/json"),
150 body,
151 ))
152 }
153
154 pub fn patch_json<B: Serialize>(
156 auth: &HttpAuthBearer,
157 url: Url,
158 body: &B,
159 ) -> Result<Self, GmailSendError> {
160 let body = serde_json::to_vec(body).map_err(GmailSendError::SerializeRequest)?;
161 Ok(Self::with_method(
162 auth,
163 "PATCH",
164 url,
165 Some("application/json"),
166 body,
167 ))
168 }
169
170 pub fn with_method(
172 auth: &HttpAuthBearer,
173 method: &str,
174 url: Url,
175 content_type: Option<&str>,
176 body: Vec<u8>,
177 ) -> Self {
178 let host = url.host_str().unwrap_or("localhost");
179
180 let mut request = HttpRequest::get(url.clone())
181 .header("Host", host)
182 .header("Accept", "application/json")
183 .header("Authorization", auth.to_authorization())
184 .body(body);
185
186 if let Some(content_type) = content_type {
187 request = request.header("Content-Type", content_type);
188 }
189
190 request.method = method.into();
191
192 debug!("prepare request to send");
193 trace!("method: {method}");
194 trace!("url: {url}");
195
196 Self {
197 state: State::Send(Http11Send::new(request)),
198 _phantom: PhantomData,
199 }
200 }
201}
202
203impl<T: DeserializeOwned> GmailCoroutine for GmailSend<T> {
204 type Yield = GmailYield;
205 type Return = Result<GmailSendOutput<T>, GmailSendError>;
206
207 fn resume(&mut self, arg: Option<&[u8]>) -> GmailCoroutineState<Self::Yield, Self::Return> {
208 match &mut self.state {
209 State::Send(send) => match send.resume(arg) {
210 HttpCoroutineState::Yielded(HttpSendYield::WantsRead) => {
211 GmailCoroutineState::Yielded(GmailYield::WantsRead)
212 }
213 HttpCoroutineState::Yielded(HttpSendYield::WantsWrite(bytes)) => {
214 GmailCoroutineState::Yielded(GmailYield::WantsWrite(bytes))
215 }
216 HttpCoroutineState::Yielded(HttpSendYield::WantsRedirect { .. }) => {
217 GmailCoroutineState::Complete(Err(GmailSendError::UnexpectedRedirect))
218 }
219 HttpCoroutineState::Complete(Err(err)) => {
220 GmailCoroutineState::Complete(Err(err.into()))
221 }
222 HttpCoroutineState::Complete(Ok(HttpSendOutput {
223 response,
224 keep_alive,
225 ..
226 })) => {
227 if response.status.is_success() {
228 let body = if response.body.is_empty() {
236 b"{}".as_slice()
237 } else {
238 response.body.as_slice()
239 };
240
241 match serde_json::from_slice::<T>(body) {
242 Ok(response) => GmailCoroutineState::Complete(Ok(GmailSendOutput {
243 response,
244 keep_alive,
245 })),
246 Err(err) => GmailCoroutineState::Complete(Err(
247 GmailSendError::ParseResponse(err),
248 )),
249 }
250 } else {
251 let (status, message) = parse_api_error(*response.status, &response.body);
252 GmailCoroutineState::Complete(Err(GmailSendError::Api { status, message }))
253 }
254 }
255 },
256 }
257 }
258}
259
260enum State {
261 Send(Http11Send),
262}
263
264#[derive(Debug, Deserialize)]
265struct ErrorEnvelope {
266 error: ErrorBody,
267}
268
269#[derive(Debug, Deserialize)]
270struct ErrorBody {
271 code: Option<u16>,
272 message: Option<String>,
273}
274
275pub fn parse_api_error(http_status: u16, body: &[u8]) -> (u16, String) {
278 if let Ok(envelope) = serde_json::from_slice::<ErrorEnvelope>(body) {
279 let status = envelope.error.code.unwrap_or(http_status);
280 let message = envelope
281 .error
282 .message
283 .filter(|message| !message.trim().is_empty())
284 .unwrap_or_else(|| String::from("unknown Gmail API error"));
285 return (status, message);
286 }
287
288 let message = String::from_utf8_lossy(body).trim().to_string();
289
290 if message.is_empty() {
291 (http_status, String::from("unknown Gmail API error"))
292 } else {
293 (http_status, message)
294 }
295}