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:

Messages:

Drafts:

Threads:

Labels and history:

Settings:

Upload aliases:

Internal parlel endpoints:

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.

FeatureStatus
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 name409 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:

HTTPstatusTypical cause
400INVALID_ARGUMENTMissing required field (topicName, startHistoryId, label name) or invalid label mutation.
404NOT_FOUNDMissing user resource, message, draft, thread, label, or settings child resource.
405FAILED_PRECONDITIONValid endpoint with an unsupported HTTP method.
409ALREADY_EXISTSCreating a label whose name already exists.
500INTERNALUnexpected server exception.

Manifest

See services/gmail/manifest.json:

<!-- parlel:testenv:start -->

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 -->