Metadata-Version: 2.4
Name: mock-gmail-api
Version: 0.1.0
Summary: Mock Gmail REST API server + seeded synthetic email fixture generator
Author: Alex Garcia
License-Expression: MIT
Project-URL: Repository, https://github.com/garcia-alex/mock-gmail-api
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: faker>=40.27.0
Requires-Dist: fastapi>=0.121.0
Requires-Dist: openpyxl>=3.1.5
Requires-Dist: python-docx>=1.2.0
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: reportlab>=5.0.0
Requires-Dist: uvicorn>=0.38.0
Dynamic: license-file

# mock-gmail-api

> This is a public-source project — see [CONTRIBUTING.md](CONTRIBUTING.md)
> for the PR policy.

A mock Gmail REST API server, plus a seeded synthetic data generator, for
developing and testing your own Python code entirely against synthetic
data. It mimics real Gmail's `/gmail/v1/users/me/...` paths and JSON shapes
closely enough that calling code written against this mock runs unmodified
against the real Gmail API — the only change is which client gets
constructed, at a single `ENV=dev`/`ENV=prod` branch point (see
[Calling it from Python](#calling-it-from-python)). It exists so a
Stage-0 pitch-deck-inbound routing feature (or similar future
tear-sheet-draft features) can be built and tested on a dev server with
zero real Gmail credentials, then run unmodified on your own prod server.

## Quickstart

```bash
git clone <this-repo>
cd mock-gmail-api
uv sync
make generate
make serve
```

Then, in another shell:

```bash
curl -H "Authorization: Bearer dev-secret-token" \
  "http://localhost:8000/gmail/v1/users/me/messages?q=to:pitches@acme.example"
```

## Install

```bash
git clone <this-repo>
cd mock-gmail-api
uv sync
```

Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/).

## Generating synthetic data

```bash
make generate
# equivalent to:
uv run mock-gmail-api generate --seed 42 --volume 200 --pitch-ratio 0.3 --db ./fixtures.sqlite
```

- `--seed`: Faker's RNG seed. The same seed always produces a
  byte-identical database — message dates are computed relative to a fixed
  reference timestamp baked into the generator, not wall-clock time, so
  regenerating on a different day doesn't change the output.
- `--volume`: approximate total number of messages generated.
- `--pitch-ratio`: fraction of generated threads that are VC pitch-inbound
  mail (company/sector/ask/stage) rather than filler (newsletters, internal
  mail, calendar invites, promos).
- `--if-missing`: skip regeneration if `--db` already exists — used by the
  Docker entrypoint so a mounted volume persists fixtures across container
  restarts.
- `--docgen-backend`: `live` (default) or `stub` — see
  [Attachment content generation](#attachment-content-generation).
- `--docgen-cache-dir`: where generated attachment content is cached
  (default `.docgen_cache/`, gitignored).

A sample `fixtures.sqlite` (seed 42, volume 200, generated with
`--docgen-backend stub` to avoid a live `claude -p` bill for a repo asset) is
committed to this repo so `make serve` works with zero setup. Regenerate it
with the live backend (`make generate`, no `--docgen-backend` flag) if you
want genuinely realistic attachment content.

## Attachment content generation

> **This is mock-only, dev-fixture code.** It has no relationship to the
> real Gmail API. See `src/mock_gmail_api/docgen/__init__.py`'s module
> docstring.

Pitch-inbound attachments (`_Pitch_Deck.pdf`, `_Cap_Table.xlsx`,
`_One_Pager.docx`) carry real, structurally valid pdf/docx/xlsx content —
company-specific pitch decks, cap tables, and one-pager memos — instead of
`MOCKATTACHMENTDATA` filler bytes. This exists so a downstream document
classifier (one that ports bronze artifacts to markdown) has something
non-trivial to actually summarize when developed against this mock.

Two content backends, selected by `--docgen-backend` /
`MOCK_GMAIL_DOCGEN_BACKEND`:

- `live` (default): shells out to `claude -p` for genuinely
  sector-specific, plausible prose/data. Requires the `claude` CLI on
  `PATH` and configured credentials.
- `stub`: fabricates plausible-shaped content with Faker only, no
  subprocess/network call. Used by this repo's own test suite so `make
  test` stays fast and offline — see `tests/conftest.py`.

Generated content is cached to disk (`.docgen_cache/` by default, keyed by
a hash of `doc_type` + company/sector/stage/ask) so re-running `generate`
with the same seed reuses previously generated bytes rather than
re-invoking `claude -p` on every run — this is also what makes
`--seed`'s determinism guarantee hold for attachments, not just message
rows.

## Running the server

```bash
make serve
# equivalent to:
uv run mock-gmail-api serve --db ./fixtures.sqlite
```

Serves on `http://localhost:8000` by default. Set `MOCK_GMAIL_DEV_TOKEN` to
require a specific bearer token (see [Auth](#auth) below).

## Endpoint reference

All endpoints below are under `/gmail/v1/users/me/`, mirroring real Gmail's
per-account REST paths. `GET /health` is separate and unauthenticated.

- `GET /messages` — search/list (`q=`, `pageToken=`, `maxResults=`)
- `GET /messages/{id}` — full message
- `GET /threads/{id}` — full thread (messages in send order)
- `GET /labels` — system + in-use labels
- `POST /drafts` — create a draft (never send)
- `GET /messages/{id}/attachments/{attachmentId}` — generated attachment
  bytes (see [Attachment content generation](#attachment-content-generation))

## Calling it from Python

### Minimal `requests` example

```python
import requests

resp = requests.get(
    "http://localhost:8000/gmail/v1/users/me/messages",
    params={"q": "to:pitches@acme.example newer_than:1d"},
    headers={"Authorization": "Bearer dev-secret-token"},
    timeout=10,
)
resp.raise_for_status()
print(resp.json())
```

### The `ENV=dev`/`ENV=prod` pattern

[`examples/dev_prod_client.py`](examples/dev_prod_client.py) is a worked,
copy-pasteable template for a consuming project to drop into its own repo
— this repo does not import it. It exposes `get_gmail_client(env)`,
returning an object with identically-shaped
`search_messages`/`get_thread`/`create_draft` methods in both branches:

- `env == "dev"`: a small `requests`-based client hitting this mock's HTTP
  API.
- `env == "prod"`: a real `googleapiclient.discovery.build("gmail", "v1",
  credentials=...)` service. Credential loading is left as a
  `NotImplementedError` stub — that OAuth/token-refresh plumbing is the
  consuming project's own responsibility on the prod server, per the
  trust-boundary model this repo exists to support. This repo's own tests
  never exercise the prod branch.

Run it against a local mock server with:

```bash
ENV=dev MOCK_GMAIL_BASE_URL=http://localhost:8000 \
  MOCK_GMAIL_DEV_TOKEN=dev-secret-token \
  uv run python examples/dev_prod_client.py
```

## Search query grammar

`q=` supports a subset of real Gmail's search operators — enough to cover
the Stage-0 skill query this mock was built for, `to:pitches@acme.example
newer_than:1d`, plus a few more for testing:

- `to:<addr>`, `from:<addr>`, `subject:<text>` — substring match
- `newer_than:<N><h|d|m>`, `older_than:<N><h|d|m>` — relative date window
- `label:<LABEL>` — case-sensitive exact label match
- `is:unread` — sugar for `label:UNREAD`
- `has:attachment` — messages with at least one attachment
- anything else is treated as a free-text term matched against
  subject/from/body

Unknown or malformed operator tokens are silently folded into free-text
(logged, not errored), matching real Gmail's lenient parser. No free-text
body search beyond that free-text fallback — this is not a full-text index.

## Fault injection

Ported from mock-superhuman-mcp's `FaultProfile`. Enable server-wide via CLI
flags or env vars:

```bash
uv run mock-gmail-api serve --db ./fixtures.sqlite \
  --faults rate-limit,timeout --fault-chance 0.1
# or: MOCK_GMAIL_FAULTS=rate-limit,timeout MOCK_GMAIL_FAULT_CHANCE=0.1
```

Fault types: `rate-limit` (raises HTTP 429), `timeout` (sleeps 1-3s before
responding), `malformed` (strips `id` from the response, adds
`__malformed__: true`), `duplicate-page` (duplicates the first item in a
`messages` list response). Each enabled fault independently has probability
`--fault-chance` of firing per eligible request; at most one fault fires per
request.

Override per-request with headers, regardless of server-wide config:

```bash
curl -H "X-Mock-Faults: rate-limit" -H "X-Mock-Fault-Chance: 1.0" ...
```

`X-Mock-Faults: none` disables faults for that request even if the server
has faults enabled globally.

## Docker

```bash
make docker-build
make docker-run
```

The image bakes in both the generator and server. Its entrypoint always
runs `generate --if-missing` before `serve`, so the same image works
whether or not a fixture volume is mounted — a fresh container without a
volume gets a freshly generated DB; one with a persisted `/data` volume
keeps its existing fixtures across restarts.

The entrypoint does not set `MOCK_GMAIL_DOCGEN_BACKEND`, so a fresh
container defaults to the `live` backend and needs a working `claude` CLI
+ credentials to generate its first fixture DB. Set
`MOCK_GMAIL_DOCGEN_BACKEND=stub` in the container environment if that's not
available (e.g. CI), or mount a `/data` volume pre-seeded by `make
generate` on the host so `--if-missing` skips generation entirely.

See [`docker-compose.example.yml`](docker-compose.example.yml) for a
documented standalone service block (env vars, named volume) to copy into
your own compose setup — this repo does not ship its own compose stack.

## Scope / Non-goals

This mock deliberately implements a narrow slice of the Gmail API:

- **No `messages.send`** — `drafts.create` only, ever. This matches a
  common policy of never sending mail programmatically; the weekly
  tear-sheet feature only ever creates drafts.
- No labels-write / `messages.modify`, no history/watch/push, no settings
  endpoints.
- No real OAuth2 — see [Auth](#auth).
- Single-inbox (`users/me`) only — no multi-mailbox concept, matching real
  Gmail's per-account API surface (unlike sibling repo
  `mock-superhuman-mcp`, which is multi-mailbox by design).
- Attachment bytes are real, generated pdf/docx/xlsx content (see
  [Attachment content generation](#attachment-content-generation) below) —
  fabricated, not real deal data, but structurally valid documents rather
  than filler bytes.
- Pitch metadata (company/sector/ask/stage) is exposed only via the
  `X-Pitch-Meta` header inside `payload.headers`, never as a top-level
  JSON field — this is intentional: it forces real extraction logic to
  actually parse body/PDF text rather than shortcut past the thing Stage-0
  is meant to exercise.
- `drafts.create` accepts a simplified JSON body (`to`/`subject`/
  `body_text`/`body_html`/`thread_id`), not the real API's strict RFC822
  `raw` (base64url MIME) field.

## Auth

Trivial bearer-token check, not real OAuth2 — there is no `/token` or
`/authorize` endpoint. Every route except `/health` requires an
`Authorization: Bearer <token>` header:

- If `MOCK_GMAIL_DEV_TOKEN` is set, the token must match it exactly (401
  otherwise).
- If unset, any nonempty token is accepted.

## Development

```bash
make lint   # ruff check + ruff format --check + pyright
make test   # pytest
```

## Trust-boundary note

This repo is dev-only and holds zero real credentials — it exists purely to
let a consuming project's Python code be developed and tested against
synthetic data before running unmodified against real Gmail on that
project's own prod server. This mirrors a common dev/prod split: a dev
server (run by the tool builder, synthetic data only) and a prod server
(run by the consuming project, real credentials, never touched by the tool
builder). No Gmail OAuth flow, real client secret, or production data
belongs in this repo.
