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