Gmail
Lightweight, dependency-free in-process fake of the Gmail v1 REST API for testing googleapis Gmail integrations with zero external side effects.
Default port: 4610
Quick start
import { google } from "googleapis";
import { GmailServer } from "./services/gmail/src/server.js";
const server = new GmailServer(4610);
await server.start();
const gmail = google.gmail({ version: "v1", auth: "fake-token" });
gmail.context._options.rootUrl = "http://127.0.0.1:4610/";
const created = await gmail.users.messages.insert({
userId: "me",
requestBody: {
raw: Buffer.from("Subject: hi\r\n\r\nhello").toString("base64url"),
labelIds: ["INBOX"],
},
});
console.log(created.data.id);
await server.stop();
State is in-memory and ephemeral. Reset it with server.reset() or POST /_parlel/reset.
Implemented operations
Profile and watch:
GET /gmail/v1/users/{userId}/profile-users.getProfilePOST /gmail/v1/users/{userId}/watch-users.watch(requirestopicName)POST /gmail/v1/users/{userId}/stop-users.stop
Messages:
GET /gmail/v1/users/{userId}/messages-users.messages.listGET /gmail/v1/users/{userId}/messages/{id}-users.messages.get(format:full,metadata,minimal,raw)POST /gmail/v1/users/{userId}/messages/send-users.messages.sendPOST /gmail/v1/users/{userId}/messages/import-users.messages.importPOST /gmail/v1/users/{userId}/messages/insert-users.messages.insertPOST /gmail/v1/users/{userId}/messages/{id}/modify-users.messages.modifyPOST /gmail/v1/users/{userId}/messages/{id}/trash-users.messages.trashPOST /gmail/v1/users/{userId}/messages/{id}/untrash-users.messages.untrashDELETE /gmail/v1/users/{userId}/messages/{id}-users.messages.deletePOST /gmail/v1/users/{userId}/messages/batchModify-users.messages.batchModifyPOST /gmail/v1/users/{userId}/messages/batchDelete-users.messages.batchDeleteGET /gmail/v1/users/{userId}/messages/{id}/attachments/{attachmentId}-users.messages.attachments.get
Drafts:
GET /gmail/v1/users/{userId}/drafts-users.drafts.listPOST /gmail/v1/users/{userId}/drafts-users.drafts.createGET /gmail/v1/users/{userId}/drafts/{id}-users.drafts.getPUT /gmail/v1/users/{userId}/drafts/{id}-users.drafts.updatePOST /gmail/v1/users/{userId}/drafts/send-users.drafts.sendDELETE /gmail/v1/users/{userId}/drafts/{id}-users.drafts.delete
Threads:
GET /gmail/v1/users/{userId}/threads-users.threads.listGET /gmail/v1/users/{userId}/threads/{id}-users.threads.getPOST /gmail/v1/users/{userId}/threads/{id}/modify-users.threads.modifyPOST /gmail/v1/users/{userId}/threads/{id}/trash-users.threads.trashPOST /gmail/v1/users/{userId}/threads/{id}/untrash-users.threads.untrashDELETE /gmail/v1/users/{userId}/threads/{id}-users.threads.delete
Labels and history:
GET /gmail/v1/users/{userId}/labels-users.labels.listPOST /gmail/v1/users/{userId}/labels-users.labels.create(duplicatename→409)GET /gmail/v1/users/{userId}/labels/{id}-users.labels.getPATCH /gmail/v1/users/{userId}/labels/{id}-users.labels.patchPUT /gmail/v1/users/{userId}/labels/{id}-users.labels.updateDELETE /gmail/v1/users/{userId}/labels/{id}-users.labels.deleteGET /gmail/v1/users/{userId}/history-users.history.list(startHistoryIdis required →400if omitted)
Settings:
GET|PUT /gmail/v1/users/{userId}/settings/autoForwarding-getAutoForwarding,updateAutoForwardingGET|PUT /gmail/v1/users/{userId}/settings/imap-getImap,updateImapGET|PUT /gmail/v1/users/{userId}/settings/language-getLanguage,updateLanguageGET|PUT /gmail/v1/users/{userId}/settings/pop-getPop,updatePopGET|PUT /gmail/v1/users/{userId}/settings/vacation-getVacation,updateVacationGET|POST|GET by id|DELETE /gmail/v1/users/{userId}/settings/filters- filter list/create/get/deleteGET|POST|GET by email|DELETE /gmail/v1/users/{userId}/settings/forwardingAddresses- forwarding address list/create/get/deleteGET|POST|GET by email|DELETE /gmail/v1/users/{userId}/settings/delegates- delegate list/create/get/deleteGET|POST|GET by email|PATCH|PUT|DELETE /gmail/v1/users/{userId}/settings/sendAs- send-as list/create/get/patch/update/deletePOST /gmail/v1/users/{userId}/settings/sendAs/{sendAsEmail}/verify- send-as verifyGET|POST|GET by id|DELETE /gmail/v1/users/{userId}/settings/sendAs/{sendAsEmail}/smimeInfo- S/MIME list/insert/get/deletePOST /gmail/v1/users/{userId}/settings/sendAs/{sendAsEmail}/smimeInfo/{id}/setDefault- S/MIME set defaultGET|POST|GET by email|PATCH|DELETE /gmail/v1/users/{userId}/settings/cse/identities- CSE identity list/create/get/patch/deleteGET|POST|GET by id /gmail/v1/users/{userId}/settings/cse/keypairs- CSE keypair list/create/getPOST /gmail/v1/users/{userId}/settings/cse/keypairs/{keyPairId}/disable- CSE keypair disablePOST /gmail/v1/users/{userId}/settings/cse/keypairs/{keyPairId}/enable- CSE keypair enablePOST /gmail/v1/users/{userId}/settings/cse/keypairs/{keyPairId}/obliterate- CSE keypair obliterate
Upload aliases:
/upload/gmail/v1/...is routed to the same handlers as/gmail/v1/...for media-capablegoogleapismethods.
Internal parlel endpoints:
GET /_parlel/healthPOST /_parlel/reset
Access via MCP / preview URL
When run inside a parlel sandbox the service is reachable at its preview URL
(the GMAIL_EMULATOR_HOST env var, e.g. http://127.0.0.1:4610). Point the
googleapis Gmail client at that address by overriding
gmail.context._options.rootUrl. MCP-driven agents can call any documented
endpoint directly; the /_parlel/reset control endpoint clears state between
scenarios.
Surface coverage
This emulator faithfully replicates the Gmail v1 surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.
Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.
| Feature | Status |
|---|---|
users.getProfile / watch / stop | ✅ Supported |
users.messages.* (list/get/send/insert/import/modify/trash/untrash/delete/batchModify/batchDelete) | ✅ Supported |
users.messages.attachments.get | ✅ Supported |
users.drafts.* (create/list/get/update/send/delete) | ✅ Supported |
users.threads.* (list/get/modify/trash/untrash/delete) | ✅ Supported |
users.labels.* (list/create/get/patch/update/delete) | ✅ Supported |
Duplicate label name → 409 ALREADY_EXISTS | ✅ Supported |
users.history.list (startHistoryId required → 400) | ✅ Supported |
users.settings.* (autoForwarding/imap/language/pop/vacation/filters/forwardingAddresses/delegates/sendAs/smimeInfo/cse) | ✅ Supported |
Message format (full/metadata/minimal/raw) | ✅ Supported |
List pagination (maxResults/pageToken) | ✅ Supported |
Google JSON error envelope (error.{code,message,errors,status} with canonical status) | ✅ Supported |
/gmail/v1 + /upload/gmail/v1 routing | ✅ Supported |
Gmail query (from:/to:/subject:/substring) | ◐ Accepted — a subset of the real Gmail search grammar |
| Opaque base64 page tokens | ✓ By design — deterministic numeric offsets keep tests reproducible |
OAuth / IAM enforcement (401 UNAUTHENTICATED) | ✓ By design — any credential is accepted; no real secrets needed |
Real email delivery / Pub/Sub watch delivery | ✓ By design — send and watch mutate local state only |
| Full MIME parsing | ✓ By design — lightweight header/body parsing, realistic enough for client tests |
| Persistent storage | ✓ By design — state is process-local and resettable |
| History pagination across all change types | ⟳ Roadmap |
Error codes & shapes
Errors use the Gmail/Google JSON API envelope, with a canonical google.rpc.Code
in error.status:
{
"error": {
"code": 404,
"message": "Message not found",
"errors": [
{ "message": "Message not found", "domain": "global", "reason": "notFound" }
],
"status": "NOT_FOUND"
}
}
Common returned codes:
| HTTP | status | Typical cause |
|---|---|---|
400 | INVALID_ARGUMENT | Missing required field (topicName, startHistoryId, label name) or invalid label mutation. |
404 | NOT_FOUND | Missing user resource, message, draft, thread, label, or settings child resource. |
405 | FAILED_PRECONDITION | Valid endpoint with an unsupported HTTP method. |
409 | ALREADY_EXISTS | Creating a label whose name already exists. |
500 | INTERNAL | Unexpected server exception. |
Manifest
See services/gmail/manifest.json:
- name:
gmail, image:parlel/gmail:0.1 - port:
4610, protocol:http, healthcheck:/_parlel/health, startup ≈ 100ms - env:
GMAIL_EMULATOR_HOST,GOOGLE_CLOUD_PROJECT,GCLOUD_PROJECT
Configuration — test.env
Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.
GMAIL_EMULATOR_HOST=http://parlel-bridge:4610
GOOGLE_CLOUD_PROJECT=parlel
GCLOUD_PROJECT=parlel
<!-- parlel:testenv:end -->