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