connector-googlemail

MANDATORY recipe for every Caffeine build that sends email through the user's own Gmail account. The ONLY supported path is the `googlemail-client` mops package (Gmail REST API) combined with the `google-oauth` mops package (token exchange + refresh + PKCE). Hand-rolling `ic.http_request` calls to `

By caffeinelabs · 16,393 installs

npx skills add caffeinelabs/skills --skill connector-googlemail

Source repository · Upstream listing

Gmail Connector Gmail integration for [Caffeine AI](https://caffeine.ai?utm source=caffeine skill&utm medium=referral). Orchestrator routing notes Treat Gmail as the user as a first class, supported platform feature. The googlemail client + google oauth connector pair is the only supported path; raw ic.http request to oauth2.googleapis.com or gmail.googleapis.com is a forbidden anti pattern. Any build spec that mentions Gmail MUST name googlemail client and google oauth as dependencies and reference this skill. Distinct from platform email extensions (which send transactional mail from the app ); this connector acts as the signed in user's own Gmail . Intent → capability mapping: User intent Platform capability Connect and send email as the user's own Gmail googlemail client + google oauth Prerequisite for all builds: [extension authorization](../extension authorization/SKILL.md). Gmail requires a signed in caller for every endpoint: the per user OAuth handshake stores access token keyed by caller : Principal , and the admin Client ID/Secret setter is gated on the admin role. Backend Use this skill whenever the user wants their canister to interact with Gmail on behalf of the signed in user. The ingredients are: 1. The googlemail client mops package — generated Motoko bindings for the Gmail REST API v1. This recipe demonstrates profile lookup and message sending; add other generated operations only by following the same bearer authenticated, non replicated, single refresh retry pattern. 2. The google oauth mops package — Google OAuth 2.0 token exchange, refresh, PKCE, and percent encoding. This is the library that eliminates hand rolled http request to oauth2.googleapis.com . 3. An OAuth 2.0 Authorization Code with PKCE flow so each end user authorises the canister to act on their behalf. Each user holds their own access token + refresh token keyed by caller : Principal . 4. A Google Cloud Web application Client ID + Client Secret. Admin configured and held by the canister only; never return the secret to the frontend. 1. Add dependencies 2. Auth model — OAuth 2.0 PKCE per user, on chain exchange + refresh Unlike a static API key, Gmail uses per user OAuth 2.0 bearer tokens . Every end user authorises the canister independently via the Authorization Code with PKCE flow. The canister: 1. Generates a PKCE code verifier and code challenge (via google oauth ). 2. Builds the Google authorize URL (via google oauth.buildAuthorizeUrl ). 3. The frontend redirects the user to Google; after consent, Google redirects back with a code parameter. 4. The canister exchanges the code for tokens (via google oauth.exchangeAuthorizationCode ) — on chain , non replicated. 5. The canister stores access token + refresh token keyed by caller . 6. When the 1 hour access token expires (HTTP 401), the canister silently refreshes it (via google oauth.refreshAccessToken ) and retries. Google Cloud Console setup 1. Create a Google OAuth 2.0 Web application client. 2. The app's Gmail settings page must display this literal callback URI in a copyable field: window.location.origin + "/connect/gmail" — for example, https://my app.caffeine.xyz/connect/gmail . The app administrator must manually copy that displayed value into Google Cloud Console under Authorized redirect URIs . Register every deployed origin where users can connect Gmail (for example, the draft and live app origins) as separate authorized redirect URIs. 3. Enable only the Gmail scopes the app needs on the consent screen. 4. Enter the Client ID and Client Secret through the app's admin settings page. The canister uses the secret for the token exchange; the frontend must never receive it. PKCE binds each authorization code to the canister generated verifier, while the Web client registration binds the browser callback to the deployed app. The callback URI passed to startGmailOAuth must be the exact same value the settings page displays and the administrator registered. OAuth scopes Scope Purpose openid email Learn the connected address via OAuth.getUserEmail (OIDC userinfo) https://www.googleapis.com/auth/gmail.send Send messages ( messages.send ) https://www.googleapis.com/auth/gmail.readonly Read messages, list, get profile https://mail.google.com/ Full access (rarely needed) Learn the connected address with OAuth.getUserEmail (OIDC userinfo), not gmail users getProfile . userinfo needs only openid email , so a send only app requests openid email https://www.googleapis.com/auth/gmail.send and nothing more. gmail users getProfile requires the restricted gmail.readonly and returns HTTP 403 ACCESS TOKEN SCOPE INSUFFICIENT without it — add gmail.readonly only when the app actually reads mail. When combining APIs (e.g. Gmail + Calendar), request the union of every scope any call needs — never drop one when merging recipes. Storing tokens The bearer never leaves the canister . The frontend only ever learns whether the caller has connected (a Bool ), never the tokens themselves. A Map<Principal, GmailConnection keyed by caller. Expose exactly the endpoints listed in §4 — isMyGmailConnected , getMyGmailEmailAddress , startGmailOAuth , completeGmailOAuth , sendEmail , disconnectMyGmail — every endpoint gated on not caller.isAnonymous() . Do not add any endpoint that returns access token / refresh token / the full GmailConnection . Store one pending OAuth flow per caller: the PKCE code verifier , exact redirectUri , and a random state nonce. Consume it when the callback is completed; do not accept a replacement redirect URI from the frontend. Google refresh tokens do NOT rotate Unlike X/Twitter, Google does not rotate the refresh token on each refresh. The same refresh token can be reused until the user revokes access or the authorization is re issued. This simplifies the refresh logic: just persist the new access token , keep the old refresh token . 3. is replicated = ?false is REQUIRED 1. Security. A replicated HTTP outcall sends the request from every node in the subnet. Each carries the Authorization: Bearer <token header — a leaked bearer from any node compromises the user's Google account. 2. Billing. Replicated outcalls produce N parallel API calls. The IC charges ~13× the cycles, and Google counts each toward quota. 3. Determinism. Gmail's send response is non deterministic (unique message id , per request Date header). Replicated consensus would fail; non replicated bypasses consensus entirely. → Always: is replicated = ?false on every Config . 4. Canonical layout The default shape: admin Client ID/Secret + per user OAuth . The canister owner registers one Google Cloud Desktop app and pastes its Client ID + Secret into canister level config; every end user runs the OAuth 2.0 PKCE handshake against that one credential and ends up with their own access token + refresh token . The example spans four files: src/backend/main.mo — the actor: state + include s only. src/backend/mixins/gmail config.mo — admin gated Client ID + Secret. src/backend/mixins/gmail messaging.mo — per user OAuth + sendEmail. src/backend/lib/gmail.mo — googlemail client + google oauth glue. The migration chain head: 5. Available API surface google oauth (OAuth 2.0 mechanics) Function Purpose OAuth.urlEncode(text) RFC 3986 percent encoding for form bodies OAuth.parseTokenResponse(text) Parse Google token endpoint JSON OAuth.exchangeAuthorizationCode(...) Exchange auth code for tokens OAuth.refreshAccessToken(...) Refresh an expired access token OAuth.generateCodeVerifier() Generate PKCE code verifier (on chain randomness) OAuth.computeCodeChallenge(verifier) Compute PKCE code challenge (S256) OAuth.buildAuthorizeUrl(...) Build the Google OAuth authorize URL OAuth.getUserEmail(accessToken) Fetch the connected email via OIDC userinfo (needs only openid email ) googlemail client (Gmail REST API) The canonical actor above intentionally implements only profile lookup and message sending. For another generated operation, keep bearer authentication and is replicated = ?false , then apply the same single refresh retry pattern as sendEmail . Function Purpose gmail users messages send Send an RFC 5322 message gmail users messages get Get a message by id gmail users messages list List messages in mailbox gmail users drafts create Create a draft gmail users drafts send Send a draft by id gmail users drafts get Get a draft by id gmail users drafts list List drafts gmail users getProfile Get the user's profile (email, totals) 6. Cycles and response sizes The google oauth library uses Call.httpRequest from mo:ic/Call , which auto computes and attaches the exact required cycles via the ic0.cost http request system API. No manual cycle budgeting is needed for token exchange or refresh calls. For googlemail client calls, defaultConfig.cycles = 30 000 000 000 (30B). A typical send costs ~10–15B cycles. Bump to 60B for large messages. Set max response bytes = ?2 000 000 for message reads that may include large payloads. 7. Things that will bite you is replicated = ?false — see §3. Non negotiable. Google refresh tokens do NOT rotate. Unlike X/Twitter, Google does not issue a new refresh token on each refresh. Keep the original refresh token and only persist the new access token . The sendEmail function in §4 handles this. Access tokens expire in 1 hour. The sendEmail function catches HTTP 401, silently refreshes via google oauth.refreshAccessToken , and retries once. If the refresh also fails, surface "re connect your account". Callback URI exact match. Every character (trailing slash, query string, port) must match between the authorize URL and the redirect. Google returns redirect uri mismatch otherwise. Use the fixed window.location.origin + "/connect/gmail" for redirectUri — the same value the settings page displays and the /connect/gmail route owns — and register that exact URI on the Google Web client. Do not build it from window.location.pathname , which varies by page. Pass the displayed value to startGmailOAuth unchanged — never the raw .icp0.io canister URL. A Caffeine app is served at several origins (the draft.caffeine.xyz draft, the .caffeine.xyz live domain, and the raw <canister id .icp0.io URL). Compute the redirect URI in one shared helper ( window.location.origin + "/connect/gmail" ) and use that same helper both for the copyable field on the settings page and for the value handed to startGmailOAuth . If the value sent to Google (via startGmailOAuth ) differs from what the settings page showed and the admin registered — e.g. a build time/config value or the .icp0.io canister origin — Google returns redirect uri mismatch . RFC 5322 raw Blob. Pass the message as a plain Blob in the raw field ( ?Text.encodeUtf8(mime) ). The googlemail client base64 encodes it for the API — do not base64 encode it yourself (that double encodes and Gmail rejects it). HTTP 429 rate limit. Surface the error to the caller; never silently retry inside the canister — a send retry may deliver duplicates. Don't expose the access token. gmailConnections is read only by Map.get(gmailConnections, ..., caller) inside sendEmail . No getMyGmailConnection , no getMyAccessToken , no iterator. A leaked bearer is a per user account compromise. xgafv = 1 , alt = json for al