Skip to content

feat(provider-gmail): delete drafts under the default gmail.compose scope - #219

Merged
stevenobiajulu merged 2 commits into
mainfrom
feat/gmail-draft-delete
Oct 18, 2026
Merged

stevenobiajulu merged 2 commits into
mainfrom
feat/gmail-draft-delete

Conversation

@stevenobiajulu

Copy link
Copy Markdown
Member

Closes #218

What

  • Gmail deleteMessage, drafts only. Calls users.drafts.delete, which gmail.compose authorizes, so there's no new scope and no restricted-scope verification. It accepts a draft id (r…) or the draft's message id; a message id is resolved through users.drafts.list, following pagination. The delete is always permanent (Gmail has no draft trash), whatever hard_delete says.
  • Non-drafts refused. A sent or received message returns NOT_SUPPORTED: "Only drafts can be deleted with the current Gmail scopes … requires gmail.modify, which Agent Email does not request." No modify endpoint is called.
  • Policy unchanged. Still needs AGENT_EMAIL_DELETE_ENABLED=true plus user_explicitly_requested_deletion: true. A Gmail draft delete doesn't need AGENT_EMAIL_HARD_DELETE_ENABLED: it's irreversible, but the draft was never sent. Passing hard_delete: true still requires the hard gate.
  • IDs (acceptance item 3). update_draft, getDraft, and getDraftReplyStatus on Gmail also accept the draft's message id (the id list_emails(folder: "drafts") returns), using the same resolver. A non-draft returns NOT_A_DRAFT.
  • delete_email errors are structured. Provider failures come back as NOT_SUPPORTED / MESSAGE_NOT_FOUND / DELETE_FAILED instead of being thrown.
  • README, tool descriptions, and the OpenSpec change add-gmail-draft-delete are updated.

Out of scope: send_draft still takes only the draft id, because its duplicate-send guard keys on the id the caller passes, and accepting both ids would let the same draft get past the guard twice. The optional retire_draft helper is also left out.

Verification

  • npm run build, npm run lint, npm run test:run (all green), check:spec-coverage PASS, openspec validate add-gmail-draft-delete --strict valid.
  • New tests: 14 provider tests (mocked client) for draft id, message id, pagination, non-draft, unknown id, stale DRAFT label, the hard flag, error classification, an old client, the policy gate end to end through delete_email, and update_draft by message id. Also 2 googleapis-client tests and 3 core action error-mapping tests.
  • Live smoke on a real Gmail mailbox (default scopes, built worktree CLI, throwaway self-addressed drafts only):
    • update_draft by message id: success, subject updated
    • delete_email by draft id: {"success":true}. Running it again: MESSAGE_NOT_FOUND
    • delete_email by message id: {"success":true}, and the draft no longer appears in list_emails(folder: drafts)
    • delete_email on an inbox message: NOT_SUPPORTED naming gmail.modify
    • With the gate unset: DELETE_DISABLED
    • The smoke found that Gmail gives a draft a new message id on every drafts.update, while the draft id stays stable. An old message id therefore gets MESSAGE_NOT_FOUND after an edit. This is now documented in the README and the delete_email id description.

…cope

delete_email on Gmail now deletes drafts through users.drafts.delete, which
gmail.compose authorizes, so no new scope or restricted-scope verification
is needed. It accepts a draft id or the draft's message id (resolved through
drafts.list). The delete is permanent, as Gmail has no draft trash, and stays
behind AGENT_EMAIL_DELETE_ENABLED plus user_explicitly_requested_deletion.
Sent or received mail is refused with NOT_SUPPORTED naming gmail.modify.

update_draft, getDraft, and getDraftReplyStatus accept a draft's message id
too, so a draft found via list_emails(folder: drafts) can be edited.
delete_email now returns provider failures as structured errors.

Closes #218
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T03:02:45.206411Z 59f5870 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codecov

codecov Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.55172% with 2 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ackages/provider-gmail/src/email-gmail-provider.ts 94.59% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59f5870489

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +130 to +131
if (err instanceof MessageNotFoundError) {
return { success: false, error: { code: 'MESSAGE_NOT_FOUND', message: err.message, recoverable: false } };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize not-found errors across package copies

When a provider resolves a different installed copy of @usejunior/email-core, its MessageNotFoundError fails this instanceof check and is returned as DELETE_FAILED instead of the promised MESSAGE_NOT_FOUND. This package-duplication scenario is explicitly handled for ProviderError by the structural isProviderError guard in providers/provider.ts, so the new not-found mapping needs an equivalent structural guard rather than relying solely on class identity.

Useful? React with 👍 / 👎.

Addresses peer review on #219. googleapis retries a DELETE after a 5xx, so
a first attempt that removed the draft makes the retry see 404; that is now
a successful delete rather than MESSAGE_NOT_FOUND. A client without
drafts.list reports MessageNotFoundError for a non-draft id instead of the
raw 404. Adds an email-security MODIFIED Delete Policy delta for the
permanent-delete carve-out covering never-delivered items.

Refs #218
@stevenobiajulu

Copy link
Copy Markdown
Member Author

Peer review: Codex gpt-6.1-sol, low effort, read-only sandbox

Round 1: NEEDS-EXECUTION. The read-only sandbox blocked Vitest's temp files and GitHub access. I ran the blocked commands myself (provider-gmail 108 passed, email-core label 12 passed, both exit 0) and gave the reviewer a write-free way to run probes: node against the built dist/.

Round 2: changes requested. The reviewer backed its findings with executed node probes, including the real Gaxios retry machinery running on an in-memory adapter. All three are fixed in 78a9daf:

Severity Finding Fix
should-fix A permanent draft delete under the soft gate contradicts email-security "Default to soft delete" Added email-security MODIFIED Delete Policy delta: never-delivered items a provider cannot trash may be deleted under the soft gates; hard_delete: true still needs the hard gate; delivered mail keeps the soft default
should-fix googleapis retries DELETE after a 503. If the first attempt deleted the draft, the retry's 404 was reported as MESSAGE_NOT_FOUND A drafts.delete 404 after successful resolution now counts as success. Regression test added
nit An old client without listDrafts let the raw 404 through as NOT_FOUND Normalized to MessageNotFoundError. Test added

Probes passed for: stale and churned message ids failing closed, pagination, update_draft by message id, non-draft handling on every entry point, and no demonstrated wrong-draft deletion. A synthetic draft-id/message-id collision resolves to the draft namespace first.

Post-fix checks: build, lint, test:run (all green), spec coverage PASS, openspec validate --strict valid.

@stevenobiajulu
stevenobiajulu enabled auto-merge (squash) October 18, 2026 03:06
@stevenobiajulu
stevenobiajulu merged commit 0548158 into main Oct 18, 2026
13 checks passed
@stevenobiajulu
stevenobiajulu deleted the feat/gmail-draft-delete branch October 18, 2026 03:10
@stevenobiajulu

Copy link
Copy Markdown
Member Author

✅ Post-merge smoke passed

Merged: 05481588ac04013220b441ea39c5d093da98ed96
Built from: main @ 0548158
Smoke: clean build → lint → npm run test:run → spec coverage → live Gmail end-to-end through the built CLI (email-agent-mcp call …, mailbox steven.obiajulu@gmail.com, default gmail.readonly + gmail.compose scopes, AGENT_EMAIL_DELETE_ENABLED=true, self-addressed throwaway drafts only)

Steps

  • ✅ clean + build
  • ✅ lint
  • ✅ tests (620 + 237 + 145 + 225 + 31 passed)
  • ✅ spec coverage: all scenarios have tests
  • ✅ update_draft by message id: success, resolved to the stable draft id r-4810…, subject updated
  • ✅ delete_email by message id: {"success":true}
  • ✅ delete_email by draft id: {"success":true}. Repeating it gives MESSAGE_NOT_FOUND
  • ✅ both smoke drafts are gone from list_emails(folder: "drafts") (0 left)
  • ✅ delete_email on an inbox message: NOT_SUPPORTED naming gmail.modify; the message is still readable afterwards
  • ✅ with AGENT_EMAIL_DELETE_ENABLED unset: DELETE_DISABLED

Notes, for full disclosure:

  • The first smoke attempt hit one failure in security/watched-allowlist.test.ts > Hot reload on in-place write, an fs.watch timing test this PR doesn't touch. It passed 5/5 when run alone, and the full suite passed on rerun, so it looks like a flake under load.
  • The second attempt failed in the smoke harness itself: zsh's echo mangled \n in JSON. It was fixed by switching to printf, and the two drafts that attempt left behind were deleted.

Visual evidence: n/a (no UI)
Prod: n/a. The repo doesn't deploy on merge, so locally installed MCP servers pick this up only after a release is tagged and the package is reinstalled.
Log: /tmp/automerge-smoke-219-1792293012.log (local)

Cleanup

  • ✅ deleted remote branch feat/gmail-draft-delete
  • ✅ removed worktree .worktrees/gmail-draft-delete
  • ✅ deleted local branch (force delete after a squash merge; the PR is MERGED)

stevenobiajulu added a commit that referenced this pull request Oct 18, 2026
Releases #219 (Gmail draft-only delete under the default gmail.compose
scope), which landed on main after the 0.3.4 tag. Agents run the installed
server, not main, so draft deletion stays unavailable until this ships.
stevenobiajulu added a commit that referenced this pull request Oct 18, 2026
…gate (#221)

The package README is the one npm shows, and it still said permanent
deletion always needs AGENT_EMAIL_HARD_DELETE_ENABLED. Since #219, Gmail
draft deletes are permanent under AGENT_EMAIL_DELETE_ENABLED alone; the
repository README already says so. Found in the v0.3.5 pre-tag review.
stevenobiajulu added a commit that referenced this pull request Oct 19, 2026
The change shipped in #219 and was released in 0.3.5. Archiving folds its
requirements into the email-categorize, email-security and provider-gmail
specs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gmail provider: support deleting drafts (drafts.delete under gmail.compose)

1 participant