Skip to main content

io_gmail/v1/
client.rs

1//! Std-blocking Gmail client, gated behind the `client` feature.
2//!
3//! Wraps a `Read + Write` stream plus the bearer credential and runs
4//! the coroutines against `gmail.googleapis.com`.
5
6#[cfg(any(
7    feature = "rustls-aws",
8    feature = "rustls-ring",
9    feature = "native-tls"
10))]
11use core::time::Duration;
12use core::{any::Any, fmt};
13
14use alloc::{
15    boxed::Box,
16    string::{String, ToString},
17};
18use std::io::{self, Read, Write};
19
20use io_http::rfc6750::bearer::HttpAuthBearer;
21#[cfg(any(
22    feature = "rustls-aws",
23    feature = "rustls-ring",
24    feature = "native-tls"
25))]
26use pimalaya_stream::{std::stream::StreamStd, tls::Tls};
27use thiserror::Error;
28#[cfg(any(
29    feature = "rustls-aws",
30    feature = "rustls-ring",
31    feature = "native-tls"
32))]
33use url::Url;
34
35#[cfg(any(
36    feature = "rustls-aws",
37    feature = "rustls-ring",
38    feature = "native-tls"
39))]
40use crate::v1::send::GMAIL_API_BASE;
41use crate::{
42    coroutine::*,
43    v1::rest::labels::{
44        GmailLabel,
45        create::GmailLabelCreate,
46        delete::GmailLabelDelete,
47        get::GmailLabelGet,
48        list::{GmailLabelsList, GmailLabelsListResponse},
49        patch::GmailLabelPatch,
50        update::GmailLabelUpdate,
51    },
52    v1::rest::messages::{
53        GmailMessage, GmailMessageFormat, GmailMessageId, delete::GmailMessageDelete,
54        get::GmailMessageGet, list::GmailMessagesList, list::GmailMessagesListParams,
55        list::GmailMessagesListResponse, modify::GmailMessageModify, send::GmailMessageSend,
56        trash::GmailMessageTrash, untrash::GmailMessageUntrash,
57    },
58    v1::rest::users::{
59        get_profile::{GmailProfile, GmailProfileGet},
60        stop::GmailStop,
61        watch::{GmailWatch, GmailWatchRequest, GmailWatchResponse},
62    },
63    v1::send::{GmailNoResponse, GmailSendError, GmailSendOutput},
64};
65
66/// Errors that can occur on the std client.
67#[derive(Debug, Error)]
68pub enum GmailClientStdError {
69    /// The Gmail exchange itself failed.
70    #[error(transparent)]
71    Send(#[from] GmailSendError),
72    /// Reading from or writing to the stream failed.
73    #[error(transparent)]
74    Io(#[from] io::Error),
75    /// Opening the TCP/TLS connection failed.
76    #[cfg(any(
77        feature = "rustls-aws",
78        feature = "rustls-ring",
79        feature = "native-tls"
80    ))]
81    #[error(transparent)]
82    Tls(#[from] anyhow::Error),
83    /// The API base URL carries no host to connect to.
84    #[cfg(any(
85        feature = "rustls-aws",
86        feature = "rustls-ring",
87        feature = "native-tls"
88    ))]
89    #[error("Gmail URL `{0}` has no host")]
90    UrlMissingHost(String),
91    /// The API base URL scheme is neither http nor https.
92    #[cfg(any(
93        feature = "rustls-aws",
94        feature = "rustls-ring",
95        feature = "native-tls"
96    ))]
97    #[error("Gmail URL `{url}` has unsupported scheme `{scheme}` (expected `http` or `https`)")]
98    UrlUnsupportedScheme {
99        /// The offending URL.
100        url: String,
101        /// The unsupported scheme it carries.
102        scheme: String,
103    },
104}
105
106/// Optional settings for [`GmailClientStd::connect`]; every field has a
107/// default (the TLS backend default, and `me` as the mailbox owner).
108pub struct GmailClientStdConnectOptions {
109    /// TLS backend configuration.
110    #[cfg(any(
111        feature = "rustls-aws",
112        feature = "rustls-ring",
113        feature = "native-tls"
114    ))]
115    pub tls: Tls,
116    /// Owner of the mailbox the requests target (`me` by default).
117    pub user_id: String,
118}
119
120impl Default for GmailClientStdConnectOptions {
121    fn default() -> Self {
122        Self {
123            #[cfg(any(
124                feature = "rustls-aws",
125                feature = "rustls-ring",
126                feature = "native-tls"
127            ))]
128            tls: Tls::default(),
129            user_id: String::from("me"),
130        }
131    }
132}
133
134const READ_BUFFER_SIZE: usize = 16 * 1024;
135
136/// Standard, blocking Gmail client.
137///
138/// Owns the stream, the bearer credential and the mailbox owner; each
139/// convenience method builds the matching coroutine and runs it to
140/// completion. Coroutines without a convenience method go through
141/// [`GmailClientStd::run`].
142pub struct GmailClientStd {
143    /// The underlying TCP or TLS stream.
144    pub stream: Box<dyn GmailStream>,
145    /// The OAuth 2.0 bearer credential added to every request.
146    pub auth: HttpAuthBearer,
147    /// Owner of the mailbox the requests target (usually `me`).
148    pub user_id: String,
149}
150
151impl GmailClientStd {
152    /// Builds a client over an already-connected stream.
153    pub fn new<S: Read + Write + Send + 'static>(
154        stream: S,
155        token: impl ToString,
156        options: GmailClientStdConnectOptions,
157    ) -> Self {
158        Self {
159            stream: Box::new(stream),
160            auth: HttpAuthBearer::new(token.to_string()),
161            user_id: options.user_id,
162        }
163    }
164
165    /// Opens a TCP/TLS connection to `gmail.googleapis.com` and builds
166    /// the client around it.
167    #[cfg(any(
168        feature = "rustls-aws",
169        feature = "rustls-ring",
170        feature = "native-tls"
171    ))]
172    pub fn connect(
173        token: impl ToString,
174        options: GmailClientStdConnectOptions,
175    ) -> Result<Self, GmailClientStdError> {
176        let GmailClientStdConnectOptions { tls, user_id } = options;
177
178        let url = Url::parse(GMAIL_API_BASE).expect("Gmail API base URL is valid");
179        let host = url
180            .host_str()
181            .ok_or_else(|| GmailClientStdError::UrlMissingHost(url.to_string()))?;
182
183        let stream = match url.scheme() {
184            "http" => StreamStd::connect_tcp(host, url.port().unwrap_or(80))?,
185            "https" => StreamStd::connect_tls(host, url.port().unwrap_or(443), &tls)?,
186            scheme => {
187                return Err(GmailClientStdError::UrlUnsupportedScheme {
188                    url: url.to_string(),
189                    scheme: scheme.to_string(),
190                });
191            }
192        };
193
194        stream.set_read_timeout(Some(Duration::from_secs(30)))?;
195
196        Ok(Self {
197            stream: Box::new(stream),
198            auth: HttpAuthBearer::new(token.to_string()),
199            user_id,
200        })
201    }
202
203    /// Replaces the underlying stream, e.g. after reconnecting.
204    pub fn set_stream<S: Read + Write + Send + 'static>(&mut self, stream: S) {
205        self.stream = Box::new(stream);
206    }
207
208    /// Runs the given coroutine to completion against the stream,
209    /// reading on `WantsRead` and writing on `WantsWrite`.
210    pub fn run<C, T>(&mut self, mut coroutine: C) -> Result<GmailSendOutput<T>, GmailClientStdError>
211    where
212        C: GmailCoroutine<Yield = GmailYield, Return = Result<GmailSendOutput<T>, GmailSendError>>,
213    {
214        let mut buf = [0u8; READ_BUFFER_SIZE];
215        let mut arg: Option<&[u8]> = None;
216
217        loop {
218            match coroutine.resume(arg.take()) {
219                GmailCoroutineState::Complete(Ok(out)) => return Ok(out),
220                GmailCoroutineState::Complete(Err(err)) => return Err(err.into()),
221                GmailCoroutineState::Yielded(GmailYield::WantsRead) => {
222                    let n = self.stream.read(&mut buf)?;
223                    arg = Some(&buf[..n]);
224                }
225                GmailCoroutineState::Yielded(GmailYield::WantsWrite(bytes)) => {
226                    self.stream.write_all(&bytes)?;
227                    arg = None;
228                }
229            }
230        }
231    }
232
233    /// Gets the profile of the mailbox (`users.getProfile`).
234    pub fn profile_get(&mut self) -> Result<GmailSendOutput<GmailProfile>, GmailClientStdError> {
235        let coroutine = GmailProfileGet::new(&self.auth, &self.user_id)?;
236        self.run(coroutine)
237    }
238
239    /// Sets up Pub/Sub push notifications (`users.watch`).
240    pub fn watch(
241        &mut self,
242        request: &GmailWatchRequest,
243    ) -> Result<GmailSendOutput<GmailWatchResponse>, GmailClientStdError> {
244        let coroutine = GmailWatch::new(&self.auth, &self.user_id, request)?;
245        self.run(coroutine)
246    }
247
248    /// Stops Pub/Sub push notifications (`users.stop`).
249    pub fn stop(&mut self) -> Result<GmailSendOutput<GmailNoResponse>, GmailClientStdError> {
250        let coroutine = GmailStop::new(&self.auth, &self.user_id)?;
251        self.run(coroutine)
252    }
253
254    /// Lists the labels of the mailbox (`users.labels.list`).
255    pub fn labels_list(
256        &mut self,
257    ) -> Result<GmailSendOutput<GmailLabelsListResponse>, GmailClientStdError> {
258        let coroutine = GmailLabelsList::new(&self.auth, &self.user_id)?;
259        self.run(coroutine)
260    }
261
262    /// Gets a label by id (`users.labels.get`).
263    pub fn label_get(
264        &mut self,
265        id: &str,
266    ) -> Result<GmailSendOutput<GmailLabel>, GmailClientStdError> {
267        let coroutine = GmailLabelGet::new(&self.auth, &self.user_id, id)?;
268        self.run(coroutine)
269    }
270
271    /// Creates the given label (`users.labels.create`).
272    pub fn label_create(
273        &mut self,
274        label: &GmailLabel,
275    ) -> Result<GmailSendOutput<GmailLabel>, GmailClientStdError> {
276        let coroutine = GmailLabelCreate::new(&self.auth, &self.user_id, label)?;
277        self.run(coroutine)
278    }
279
280    /// Updates the given label in place (`users.labels.update`).
281    pub fn label_update(
282        &mut self,
283        label: &GmailLabel,
284    ) -> Result<GmailSendOutput<GmailLabel>, GmailClientStdError> {
285        let coroutine = GmailLabelUpdate::new(&self.auth, &self.user_id, label)?;
286        self.run(coroutine)
287    }
288
289    /// Patches the given label (`users.labels.patch`).
290    pub fn label_patch(
291        &mut self,
292        label: &GmailLabel,
293    ) -> Result<GmailSendOutput<GmailLabel>, GmailClientStdError> {
294        let coroutine = GmailLabelPatch::new(&self.auth, &self.user_id, label)?;
295        self.run(coroutine)
296    }
297
298    /// Deletes a label by id (`users.labels.delete`).
299    pub fn label_delete(
300        &mut self,
301        id: &str,
302    ) -> Result<GmailSendOutput<GmailNoResponse>, GmailClientStdError> {
303        let coroutine = GmailLabelDelete::new(&self.auth, &self.user_id, id)?;
304        self.run(coroutine)
305    }
306
307    /// Lists message ids matching the params (`users.messages.list`).
308    pub fn messages_list(
309        &mut self,
310        params: &GmailMessagesListParams,
311    ) -> Result<GmailSendOutput<GmailMessagesListResponse>, GmailClientStdError> {
312        let coroutine = GmailMessagesList::new(&self.auth, &self.user_id, params)?;
313        self.run(coroutine)
314    }
315
316    /// Gets a message by id (`users.messages.get`).
317    pub fn message_get(
318        &mut self,
319        id: &str,
320        format: GmailMessageFormat,
321        metadata_headers: &[&str],
322    ) -> Result<GmailSendOutput<GmailMessage>, GmailClientStdError> {
323        let coroutine =
324            GmailMessageGet::new(&self.auth, &self.user_id, id, format, metadata_headers)?;
325        self.run(coroutine)
326    }
327
328    /// Sends the given message (`users.messages.send`).
329    pub fn message_send(
330        &mut self,
331        message: &GmailMessage,
332    ) -> Result<GmailSendOutput<GmailMessageId>, GmailClientStdError> {
333        let coroutine = GmailMessageSend::new(&self.auth, &self.user_id, message)?;
334        self.run(coroutine)
335    }
336
337    /// Adds and removes labels on a message (`users.messages.modify`).
338    pub fn message_modify(
339        &mut self,
340        id: &str,
341        add_label_ids: &[String],
342        remove_label_ids: &[String],
343    ) -> Result<GmailSendOutput<GmailMessage>, GmailClientStdError> {
344        let coroutine = GmailMessageModify::new(
345            &self.auth,
346            &self.user_id,
347            id,
348            add_label_ids,
349            remove_label_ids,
350        )?;
351        self.run(coroutine)
352    }
353
354    /// Moves a message to the trash (`users.messages.trash`).
355    pub fn message_trash(
356        &mut self,
357        id: &str,
358    ) -> Result<GmailSendOutput<GmailMessage>, GmailClientStdError> {
359        let coroutine = GmailMessageTrash::new(&self.auth, &self.user_id, id)?;
360        self.run(coroutine)
361    }
362
363    /// Restores a message from the trash (`users.messages.untrash`).
364    pub fn message_untrash(
365        &mut self,
366        id: &str,
367    ) -> Result<GmailSendOutput<GmailMessage>, GmailClientStdError> {
368        let coroutine = GmailMessageUntrash::new(&self.auth, &self.user_id, id)?;
369        self.run(coroutine)
370    }
371
372    /// Permanently deletes a message (`users.messages.delete`).
373    pub fn message_delete(
374        &mut self,
375        id: &str,
376    ) -> Result<GmailSendOutput<GmailNoResponse>, GmailClientStdError> {
377        let coroutine = GmailMessageDelete::new(&self.auth, &self.user_id, id)?;
378        self.run(coroutine)
379    }
380}
381
382impl fmt::Debug for GmailClientStd {
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        f.debug_struct("GmailClientStd")
385            .field("auth", &self.auth)
386            .field("user_id", &self.user_id)
387            .finish_non_exhaustive()
388    }
389}
390
391/// Boxable client stream: `Read + Write + Send` plus `Any` so callers
392/// can downcast back to the concrete stream type.
393pub trait GmailStream: Read + Write + Send + Any {
394    /// Returns the stream as a mutable `Any` for downcasting.
395    fn as_any_mut(&mut self) -> &mut dyn Any;
396}
397
398impl<T: Read + Write + Send + Any> GmailStream for T {
399    fn as_any_mut(&mut self) -> &mut dyn Any {
400        self
401    }
402}