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() {
229 b"null".as_slice()
230 } else {
231 response.body.as_slice()
232 };
233
234 match serde_json::from_slice::<T>(body) {
235 Ok(response) => GmailCoroutineState::Complete(Ok(GmailSendOutput {
236 response,
237 keep_alive,
238 })),
239 Err(err) => GmailCoroutineState::Complete(Err(
240 GmailSendError::ParseResponse(err),
241 )),
242 }
243 } else {
244 let (status, message) = parse_api_error(*response.status, &response.body);
245 GmailCoroutineState::Complete(Err(GmailSendError::Api { status, message }))
246 }
247 }
248 },
249 }
250 }
251}
252
253enum State {
254 Send(Http11Send),
255}
256
257#[derive(Debug, Deserialize)]
258struct ErrorEnvelope {
259 error: ErrorBody,
260}
261
262#[derive(Debug, Deserialize)]
263struct ErrorBody {
264 code: Option<u16>,
265 message: Option<String>,
266}
267
268pub fn parse_api_error(http_status: u16, body: &[u8]) -> (u16, String) {
271 if let Ok(envelope) = serde_json::from_slice::<ErrorEnvelope>(body) {
272 let status = envelope.error.code.unwrap_or(http_status);
273 let message = envelope
274 .error
275 .message
276 .filter(|message| !message.trim().is_empty())
277 .unwrap_or_else(|| String::from("unknown Gmail API error"));
278 return (status, message);
279 }
280
281 let message = String::from_utf8_lossy(body).trim().to_string();
282
283 if message.is_empty() {
284 (http_status, String::from("unknown Gmail API error"))
285 } else {
286 (http_status, message)
287 }
288}