tree: 8258e85b6fa3aeb70c5a21ffd9ba363e4e4f229d
  1. src/
  2. tests/
  3. .gitignore
  4. pyproject.toml
  5. README.md
tools/gmail/oauth-draft/README.md

Table of Contents generated with DocToc

oauth-draft

Small Python project that talks directly to the Gmail REST API on a user-provided OAuth refresh token. Five console scripts:

Console scriptPurpose
oauth-draft-setupOne-time interactive OAuth consent flow that writes the credentials JSON.
oauth-draft-createCreate a Gmail draft with threadId attachment. (As of the replyToMessageId parameter on the claude.ai Gmail MCP create_draft, the MCP can also produce thread-attached drafts — see ../draft-backends.md. This script remains useful when you have a threadId on hand and would rather skip the extra get_thread round-trip the MCP path requires, and is the only path that lets the skills delete drafts via the Gmail API afterwards.)
oauth-draft-mark-readBulk-modify Gmail threads matching a search query (default: mark as read by removing the UNREAD label). No MCP equivalent today.
oauth-draft-message-idResolve the root RFC-5322 Message-ID header of one or more threads (threads.get?format=metadata). No MCP equivalent — the claude.ai Gmail MCP get_thread surfaces only Gmail's opaque per-message IDs, never the Message-ID: header. security-issue-import records the result in the Security mailing list thread tracker field.
oauth-draft-mcpExposes the same plain-text drafting as an MCP server (create_draft, plus setup_credentials / check_auth for in-MCP authentication) so any MCP-speaking agent can create Gmail drafts without the claude.ai Gmail connector (which rewrites links into tracking redirects). Needs the optional mcp extra. See MCP server below.

The strongly preferred drafting backend is this oauth_curl tool: the claude.ai Gmail MCP create_draft silently rewrites embedded URLs into Google tracking redirects, so it must not be used for drafts that contain links — see ../draft-backends.md. This README covers local-setup, day-to-day invocation, and the project's own test/lint workflow.

Run

From the framework's root (this repository when running standalone; the .apache-magpie/ snapshot path inside an adopting tracker repo):

uv run --project tools/gmail/oauth-draft oauth-draft-create \
  --thread-id <gmail-threadId> \
  --to reporter@example.com \
  --cc security@<project>.apache.org \
  --subject "Re: <root subject>" \
  --body-file /path/to/body.txt

Skill files and framework docs reference the same invocation via the <framework> placeholder so the path resolves in either context:

uv run --project <framework>/tools/gmail/oauth-draft oauth-draft-create ...

<framework> substitutes to .apache-magpie/apache-magpie in adopting projects and to . (the repository root) in framework standalone — see the placeholder convention in AGENTS.md.

The other two scripts follow the same shape:

# Bulk mark-as-read (dry-run by default; add --execute to actually modify)
uv run --project <framework>/tools/gmail/oauth-draft oauth-draft-mark-read \
  --query 'label:apache-security in:spam is:unread'

# Add --execute after reviewing the dry-run output
uv run --project <framework>/tools/gmail/oauth-draft oauth-draft-mark-read \
  --query 'label:apache-security in:spam is:unread' --execute

# Resolve the root Message-ID of one or more threads (TSV, or --json)
uv run --project <framework>/tools/gmail/oauth-draft oauth-draft-message-id \
  19e9d09a31ff6bdd 19dda947a5d6ca88

Per-flag help: oauth-draft-create --help, oauth-draft-mark-read --help, oauth-draft-message-id --help, oauth-draft-setup --help.

Setup — one-time

You need a Google OAuth client with the https://mail.google.com/ scope, and a refresh token issued against the Gmail account you use for security@<project>.apache.org triage.

  1. Create a Google Cloud project (if you don't already have one for this purpose). Enable the Gmail API.

  2. Create an OAuth client of type Desktop app. Download the credentials JSON (call it client_secrets.json).

  3. Run the consent flow with the downloaded client_secrets.json. oauth-draft-setup opens a browser tab against Google's consent screen, captures the auth code on a local-bound port, exchanges it for a refresh token, and writes the credentials file in the shape the other two scripts expect:

    uv run --project <framework>/tools/gmail/oauth-draft oauth-draft-setup \
      /path/to/client_secrets.json
    

    Optional flags:

    FlagPurpose
    --from-addressAddress baked into the credentials file as the outgoing From:. Defaults to $GMAIL_FROM, then git config user.email.
    --outOutput path. Default: ~/.config/apache-magpie/gmail-oauth.json.
    --rm-client-secretsDelete the input client_secrets.json after writing the credentials file.

    The script writes the credentials atomically with mode 600 and chmods the parent directory to 700. The refresh token it stores is the long-lived secret of the whole oauth_curl backend; treat the file like an SSH private key.

  4. Smoke-test by running a dry-run thread search:

    uv run --project <framework>/tools/gmail/oauth-draft oauth-draft-mark-read \
      --query 'in:inbox is:unread' --max 3
    

    This exercises Credentials.load → refresh_access_token → threads.list without modifying anything. A non-empty list of thread IDs (or “Found 0 matching thread(s)”) means the credentials work.

How threading is guaranteed

When oauth-draft-create is invoked with --thread-id, the script does three things, in order:

  1. Refreshes a short-lived access token from the stored refresh token.
  2. Reads the chronologically-last message in the thread and extracts its Message-ID header (and the existing References chain).
  3. Builds an RFC822 MIME message with In-Reply-To: <that-Message-ID> and References: <existing chain> <that-Message-ID>, plus sets threadId in the Gmail API call.

Gmail's server-side threader attaches by threadId; every other mail client that receives the message threads by References / In-Reply-To chain. Both paths agree, so the draft lands on the same conversation for everyone.

Pass --no-reply-headers to skip step 2 (useful only for smoke testing — production drafts always want the headers set).

MCP server

oauth-draft-mcp runs the same OAuth + REST plain-text drafting as an MCP stdio server, so an agent can create Gmail drafts by calling a tool instead of shelling out. It exposes three tools:

  • create_draft — backed by the exact build_mime / create_draft code the CLI uses, so the message is plain text only, by construction (a single text/plain part; there is no HTML / rich-text parameter and no code path that can emit one), and links go out verbatim.
  • setup_credentials — runs the one-time OAuth consent flow (the same flow as oauth-draft-setup) and writes the credentials file, so an agent can authenticate through the MCP itself, no separate shell step.
  • check_auth — exchanges the stored refresh token for an access token to confirm auth works before drafting.

Prefer create_draft over the claude.ai Gmail connector's create_draft, which rewrites embedded URLs into Google tracking redirects (see ../draft-backends.md).

The mcp SDK is an optional extra (kept out of the base deps so the stdlib-only console scripts stay lightweight). Register the server once, at user scope, so every project sees it:

claude mcp add gmail-plaintext -s user -- \
  uv run --project <framework>/tools/gmail/oauth-draft --extra mcp oauth-draft-mcp

That makes the tools available to agents under the mcp__gmail-plaintext__* prefix (e.g. mcp__gmail-plaintext__create_draft). Add that tool id to the permissions.allow list so it runs without a prompt (it only ever creates unsent drafts the human reviews and sends).

Because it is registered at user scope (-s user), it is installed for you, not for a single project — it is available to any Claude Code session on the machine, whether or not that work has anything to do with Magpie. Registering it once is enough for every repo you touch.

The server reuses the same credential file as the CLI (~/.config/apache-magpie/gmail-oauth.json). Authenticate either by running oauth-draft-setup (CLI) or by calling the setup_credentials tool once — both write the same file.

create_draft arguments: to, subject, body (required); cc, bcc, thread_id, no_reply_headers, credentials_path (optional). When thread_id is set the server derives In-Reply-To / References from the thread's last message, exactly like oauth-draft-create.

Confidentiality

The refresh token grants full read/draft access to your Gmail. Treat it like an SSH key:

  • The setup script writes the file with mode 600 and chmods its parent directory to 700; do not loosen those.
  • Do not commit the credentials file. The path lives outside the repo tree by default (~/.config/apache-magpie/gmail-oauth.json).
  • Revoke the refresh token at https://myaccount.google.com/permissions if you suspect it has leaked.

Test

cd tools/gmail/oauth-draft
uv run --group dev pytest

Lint / type-check

cd tools/gmail/oauth-draft
uv run --group dev ruff check src tests
uv run --group dev ruff format --check src tests
uv run --group dev mypy

The prek hooks configured in .pre-commit-config.yaml at the repository root run ruff check, ruff format --check, mypy, and pytest on the project files automatically on every commit that touches them.

Referenced by