| <!-- SPDX-License-Identifier: Apache-2.0 |
| https://www.apache.org/licenses/LICENSE-2.0 --> |
| |
| <!-- START doctoc generated TOC please keep comment here to allow auto update --> |
| <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> |
| **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* |
| |
| - [oauth-draft](#oauth-draft) |
| - [Run](#run) |
| - [Setup — one-time](#setup--one-time) |
| - [How threading is guaranteed](#how-threading-is-guaranteed) |
| - [MCP server](#mcp-server) |
| - [Confidentiality](#confidentiality) |
| - [Test](#test) |
| - [Lint / type-check](#lint--type-check) |
| - [Referenced by](#referenced-by) |
| |
| <!-- END doctoc generated TOC please keep comment here to allow auto update --> |
| |
| <!-- Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. --> |
| |
| # oauth-draft |
| |
| Small Python project that talks directly to the Gmail REST API on a |
| user-provided OAuth refresh token. Five console scripts: |
| |
| | Console script | Purpose | |
| |---|---| |
| | `oauth-draft-setup` | One-time interactive OAuth consent flow that writes the credentials JSON. | |
| | `oauth-draft-create` | Create 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`](../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-read` | Bulk-modify Gmail threads matching a search query (default: mark as read by removing the `UNREAD` label). No MCP equivalent today. | |
| | `oauth-draft-message-id` | Resolve 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-mcp` | Exposes 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](#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`](../draft-backends.md#privacy-warning--the-claudeai-gmail-mcp-rewrites-embedded-urls-into-google-tracking-redirects). |
| 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): |
| |
| ```bash |
| 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: |
| |
| ```bash |
| 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`](../../../AGENTS.md#placeholder-convention-used-in-skill-files). |
| |
| The other two scripts follow the same shape: |
| |
| ```bash |
| # 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: |
| |
| ```bash |
| uv run --project <framework>/tools/gmail/oauth-draft oauth-draft-setup \ |
| /path/to/client_secrets.json |
| ``` |
| |
| Optional flags: |
| |
| | Flag | Purpose | |
| |---|---| |
| | `--from-address` | Address baked into the credentials file as the outgoing `From:`. Defaults to `$GMAIL_FROM`, then `git config user.email`. | |
| | `--out` | Output path. Default: `~/.config/apache-magpie/gmail-oauth.json`. | |
| | `--rm-client-secrets` | Delete 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: |
| |
| ```bash |
| 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](https://modelcontextprotocol.io/) 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`](../draft-backends.md#privacy-warning--the-claudeai-gmail-mcp-rewrites-embedded-urls-into-google-tracking-redirects)). |
| |
| 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: |
| |
| ```bash |
| 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 |
| |
| ```bash |
| cd tools/gmail/oauth-draft |
| uv run --group dev pytest |
| ``` |
| |
| ## Lint / type-check |
| |
| ```bash |
| 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 |
| |
| - [`../operations.md`](../operations.md#drafting-backends) — two-backend overview. |
| - [`../threading.md`](../threading.md) — threading guarantees per backend. |
| - [`../draft-backends.md`](../draft-backends.md) — the config knob. |