Skip to main content

io_gmail/
lib.rs

1#![no_std]
2#![deny(missing_docs)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4
5//! # io-gmail
6//!
7//! I/O-free coroutines for the [Gmail REST API], built on [io-http]
8//! (HTTP/1.1) and pumped by any stream the caller owns.
9//!
10//! [Gmail REST API]: https://developers.google.com/gmail/api/reference/rest
11//! [io-http]: https://docs.rs/io-http
12//!
13//! io-gmail is the Gmail sibling of [io-imap] and [io-jmap]: same shape,
14//! different wire protocol (JSON over HTTP rather than IMAP or JMAP). It
15//! is consumed by io-email (as the Gmail backend of the shared email
16//! API) and directly by himalaya (the protocol-specific gmail commands).
17//!
18//! [io-imap]: https://docs.rs/io-imap
19//! [io-jmap]: https://docs.rs/io-jmap
20//!
21//! ## Layers and features
22//!
23//! The crate has two of the three standard Pimalaya layers; there is no
24//! CLI:
25//!
26//! 1. **I/O-free coroutines** (`no_std` core, always present): the whole
27//!    Gmail REST logic.
28//! 2. **Std client** ([`v1::client::GmailClientStd`], `client` feature):
29//!    a blocking pump over any stream, with `connect` opening the
30//!    TCP/TLS connection itself behind a TLS feature (`rustls-ring` by
31//!    default, `rustls-aws`, `native-tls`).
32//!
33//! ## Everything lives under v1
34//!
35//! The Gmail REST API is versioned (`/gmail/v1/`), so the crate is too:
36//! the version-agnostic [`coroutine`] contract stays at the crate root,
37//! everything else lives under [`v1`]. The day Gmail ships a v2, a
38//! sibling module slots in without breaking `v1` consumers.
39//!
40//! ## The coroutine contract
41//!
42//! Every exchange implements [`coroutine::GmailCoroutine`]: `resume`
43//! takes the bytes read since the last yield and either requests I/O
44//! ([`coroutine::GmailYield`] `WantsRead` / `WantsWrite`) or completes.
45//! The [`gmail_try!`] macro is the coroutine equivalent of `?`.
46//!
47//! A Gmail call is a single HTTP request/response, so every REST
48//! coroutine is a thin wrapper around one shared primitive,
49//! [`v1::send::GmailSend`]: it builds the authorized request (bearer
50//! token, JSON in and out) and parses either the 2xx body or Gmail's
51//! error envelope into [`v1::send::GmailSendError`]. Redirects are never
52//! followed. The terminal [`v1::send::GmailSendOutput`] carries the
53//! parsed response plus a keep-alive flag so pumps can reuse the
54//! connection across the many small requests a Gmail session makes.
55//!
56//! ## Naming
57//!
58//! Public items follow `<Domain><Target><Verb><Ext>`: the domain is
59//! `Gmail`, the target-verb pair mirrors the REST method
60//! (`GmailLabelGet` for `users.labels.get`, `GmailMessagesBatchDelete`
61//! for `users.messages.batchDelete`) and the extension distinguishes
62//! companions (`Params`, `Response`, `Error`, `Yield`). Pure data
63//! resources omit the verb (`GmailLabel`, `GmailMessage`); the target
64//! is omitted when the verb applies to the whole exchange
65//! ([`v1::send::GmailSend`], `GmailWatch`, `GmailStop`).
66//!
67//! ## Module layout
68//!
69//! [`v1::rest`] mirrors the Gmail REST reference one-to-one. The whole
70//! API hangs off the `users` resource, so that level is flattened away:
71//! each sub-resource is a directory and each method a file named after
72//! the API method in snake_case (`getProfile` becomes get_profile.rs).
73//! A reader who knows the reference knows where to look.
74//!
75//! Request bodies take the whole resource by reference (`&GmailLabel`,
76//! `&GmailMessage`), so a `Default` resource with a few fields set
77//! serializes cleanly. Enum-valued wire strings are typed enums. List
78//! methods take borrowed `*Params` structs flattened into query pairs
79//! by [`v1::query::to_query_pairs`], a tiny `no_std` serde serializer
80//! that emits the repeated-key sequences Gmail expects.
81//!
82//! ## Watching a mailbox
83//!
84//! [`v1::history_poll::GmailHistoryPoll`] is the one composite,
85//! multi-step coroutine: an infinite watch that baselines the history
86//! cursor via `users.getProfile`, polls `users.history.list` on a timer
87//! and yields one Gmail-native diff per tick, re-baselining on an
88//! expired cursor. It is the polling alternative to `users.watch` and
89//! `users.stop` (Pub/Sub push), which exist as plain coroutines for API
90//! completeness but are not wired into a watcher.
91//!
92//! ## Authentication
93//!
94//! io-gmail does no OAuth itself: the API only accepts OAuth 2.0 bearer
95//! tokens, so the credential is exactly a bare access token, and minting
96//! or refreshing it is the caller's responsibility.
97//!
98//! ## Logging
99//!
100//! Coroutines pair a `debug!` lifecycle line with one `trace!` per input
101//! variable in `new()`, and a `debug!` plus `trace!("out: ...")` when
102//! `resume` completes; the crate never logs above `debug!`.
103//!
104//! ## Example
105//!
106//! Running a coroutine against a caller-owned TLS stream:
107//!
108//! ```rust,no_run
109//! use std::{
110//!     io::{Read, Write},
111//!     net::TcpStream,
112//!     sync::Arc,
113//! };
114//!
115//! use io_gmail::{coroutine::*, v1::rest::users::get_profile::GmailProfileGet};
116//! use io_http::rfc6750::bearer::HttpAuthBearer;
117//! use rustls::{ClientConfig, ClientConnection, StreamOwned};
118//! use rustls_platform_verifier::ConfigVerifierExt;
119//!
120//! let config = ClientConfig::with_platform_verifier().unwrap();
121//! let server_name = "gmail.googleapis.com".try_into().unwrap();
122//! let conn = ClientConnection::new(Arc::new(config), server_name).unwrap();
123//! let tcp = TcpStream::connect(("gmail.googleapis.com", 443)).unwrap();
124//! let mut stream = StreamOwned::new(conn, tcp);
125//!
126//! let auth = HttpAuthBearer::new("token");
127//! let mut coroutine = GmailProfileGet::new(&auth, "me").unwrap();
128//!
129//! let mut arg: Option<&[u8]> = None;
130//! let mut buf = [0u8; 8192];
131//! let mut read = Vec::new();
132//!
133//! let out = loop {
134//!     match coroutine.resume(arg.take()) {
135//!         GmailCoroutineState::Complete(Ok(out)) => break out,
136//!         GmailCoroutineState::Complete(Err(err)) => panic!("{err}"),
137//!         GmailCoroutineState::Yielded(GmailYield::WantsRead) => {
138//!             let n = stream.read(&mut buf).unwrap();
139//!             read.clear();
140//!             read.extend_from_slice(&buf[..n]);
141//!             arg = Some(&read);
142//!         }
143//!         GmailCoroutineState::Yielded(GmailYield::WantsWrite(bytes)) => {
144//!             stream.write_all(&bytes).unwrap();
145//!         }
146//!     }
147//! };
148//!
149//! println!("email address: {}", out.response.email_address);
150//! ```
151
152extern crate alloc;
153#[cfg(feature = "client")]
154extern crate std;
155
156pub mod coroutine;
157pub mod v1;