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 . Since 0.2.0 the package ships that default. defaultConfig.is replicated is ?false , so { defaultConfig with auth = … } is already safe; the explicit assignment stays worthwhile because it keeps the requirement visible. If you are upgrading from 0.1.x, check your code for this. Before 0.2.0 defaultConfig carried is replicated = null , which means replicated , and the generator forces non replication only for PUT and DELETE — messages.send is a POST. Any app that took defaultConfig as it came sent each mail once per replica. Code that followed this skill was fine; code that did not was sending ~13 copies of every email. 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 . Two equivalent surfaces. The flat free functions are what the canonical actor above uses and remain fully supported: 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) 0.2.0 also ships the nested class Client.mo facade that googledrive and googlecalendar have, mirroring Gmail's hierarchical operationIds. It captures Config once and only re namespaces — the Apis modules stay the source of truth, so mixing the two surfaces in one actor is fine: gmail.drafts carries create / get / list / send ; gmail.messages carries get / list / send ; getProfile sits directly on Client because it has no resource segment of its own. Migrating from 0.1.6. Three optional enum query parameters — DollarPeriodxgafv ( $.xgafv ), alt , and format on the two get operations — are now ?T on both surfaces, so every call site needs a ? : Pass null instead to omit the parameter from the query string entirely, which is what the ? buys you. Non enum parameters are unchanged and still take no null . This is the same break googlecalendar took in its 0.2.0; earlier drafts of this skill said googlemail would escape it, because src/Apis/UsersApi.mo used to be frozen by .openapi generator ignore and kept its pre ?T signatures. Nothing is frozen any more — both hand patches it carried are generated now, the second of them (base64url for raw ) as of this release. 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, leave cycles unset ( null ): each call attaches the fee the IC charges for that request, sized from the message actually sent, so large messages need no manual budget, and the fee is zero on subnets where outcalls are free. ?n attaches exactly n cycles: the IC rejects the call when n is below its fee, and the call traps on a subnet where the canister holds fewer than n . 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 n