googlemail-client
Motoko bindings for the Gmail API v1, generated from Google's official OpenAPI spec.
Send-focused PoC surface: gmail_users_messages_send, gmail_users_drafts_{create,send,get,list}, gmail_users_messages_{get,list}, gmail_users_getProfile. All 8 operations live in Apis/UsersApi.mo.
Trigger phrases
Reach for this skill on any request mentioning: send email, send message, compose email, Gmail, draft, inbox, mailbox, "email the user", "notify via email", "forward results by email", "send a notification email".
How Gmail authentication works (read before wiring)
Gmail uses OAuth 2.0 Authorization Code flow — there is no static API key. The canister never mints a token on its own; the user completes an OAuth dance off-chain and passes in the resulting Bearer access token at call time.
Token lifetime: 1 hour by default (Google's access tokens). After expiry the API returns HTTP 401. The refresh token must be exchanged off-chain too — the canister cannot call Google's token endpoint (that would expose the client secret on-chain). Surface a #Err("auth_expired") result and ask the caller to re-authenticate.
Required OAuth 2.0 scope for messages.send: https://www.googleapis.com/auth/gmail.send. For read access add: https://www.googleapis.com/auth/gmail.readonly.
Usage
import { gmail_users_messages_send; gmail_users_getProfile;
gmail_users_drafts_create; gmail_users_drafts_send }
"mo:googlemail-client/Apis/UsersApi";
import { Message; type Message } "mo:googlemail-client/Models/Message";
import { Draft; type Draft } "mo:googlemail-client/Models/Draft";
import { defaultConfig } "mo:googlemail-client/Config";
import Text "mo:core/Text"; // Text.encodeUtf8: build the raw RFC 2822 Blob
// Shared cfg — swap in the caller's short-lived bearer token.
let cfg = {
defaultConfig with
auth = ?#bearer "<off-chain OAuth2 access token>";
max_response_bytes = ?500_000;
is_replicated = ?false; // non-replicated: required for sends (see Notes); reads too
};
// Send a message. `raw` is the PLAIN RFC 2822 message as a Blob — the client
// base64-encodes it for the Gmail API; do NOT base64-encode it yourself.
let mime : Text = "From: me\r\nTo: friend@example.com\r\nSubject: Hi\r\n\r\nHello!";
let outMsg = Message.init {}; // all-null base, then layer fields:
let envelope : Message = { outMsg with raw = ?Text.encodeUtf8(mime) };
let result = await* gmail_users_messages_send(cfg,
"me", // userId: "me" = authenticated user
#_1_, // $.xgafv — use #_1_ (v1) for all calls
"", // accessToken — leave "" when auth = ?#bearer above
#json, // alt
"", "", "", "", true, "", "", "", // callback/fields/key/oauthToken/prettyPrint/quotaUser/uploadProtocol/uploadType
envelope
);
// Get the authenticated user's email address
let profile = await* gmail_users_getProfile(cfg, "me",
#_1_, "", #json, "", "", "", "", true, "", "", "");
let ?email = profile.emailAddress else return #Err("no email");
Notes
- Use
is_replicated = ?false(non-replicated) for sends (gmail_users_messages_send,
gmail_users_drafts_send). These outcalls are non-idempotent and Gmail's response is non-deterministic (unique message id, per-request Date header). In replicated mode (null) every subnet replica issues the request — so the email is sent once per replica (duplicates) and the differing responses fail IC consensus ("No consensus could be reached. Replicas had different responses"). Non-replicated has a single node perform exactly one send. Reads (gmail_users_messages_list, gmail_users_messages_get, gmail_users_getProfile) also use ?false (one node, ~13× cheaper).
- The
$.xgafvparameter (version discriminator) should always be#_1_for
Gmail API v1 calls. The alt parameter should always be #json.
- All optional string parameters (
callback,fields,key,oauthToken,
quotaUser, uploadProtocol, uploadType) accept "" to omit them; prettyPrint can be false.
userId="me"refers to the authenticated user. Explicit email addresses
also work but require the https://mail.google.com/ scope.
- Messages must be in RFC 2822 format, passed as a plain
Blobin theraw
field (e.g. ?Text.encodeUtf8(mime)). The client base64-encodes raw for the API — do not base64-encode it yourself (that double-encodes and Gmail rejects it). The Message.payload / MessagePart fields are for parsed read responses — don't try to build them for sending.
- Google returns HTTP 429 on rate-limit (quota exceeded). Surface the error to
the caller; never silently retry inside the canister — a send retry may deliver duplicates.
- Access tokens expire in 1 hour. On 401, surface
#Err("auth_expired")so
the caller can re-authenticate off-chain and retry with a fresh token.
Draft.messageholds aMessage;gmail_users_drafts_createbuilds a draft
server-side. Use gmail_users_drafts_send to send a draft by its id.
max_response_bytes: Gmail message reads can be large. 500 KB covers typical
messages; bump to 2 MB for messages with large payloads (Gmail API's max response is bounded by the API, but set conservatively for cycle budgets).
- Cycle budget:
defaultConfig.cycles = 30_000_000_000(30B). On the IC,
outbound HTTPS calls cost ~10–15B cycles for a typical send. Adjust if you see InsufficientCycles errors.







