Skip to content

feat(gmail): add --draft flag to +send, +reply, +reply-all, +forward#571

Draft
malob wants to merge 2 commits intogoogleworkspace:mainfrom
malob:feat/gmail-draft-flag
Draft

feat(gmail): add --draft flag to +send, +reply, +reply-all, +forward#571
malob wants to merge 2 commits intogoogleworkspace:mainfrom
malob:feat/gmail-draft-flag

Conversation

@malob
Copy link
Contributor

@malob malob commented Mar 20, 2026

Description

Adds a --draft flag to +send, +reply, +reply-all, and +forward. When set, calls users.drafts.create instead of users.messages.send. Message construction is identical — only the API method and metadata wrapper change.

Note: Opened as a draft because this PR is based on #554 rather than main to avoid merge conflicts, since that PR rewrites much of the mail-sending code. This should not be merged directly — only the second commit (93997d9) is new work. Happy to rebase onto main once #554 merges.

How it works

  • --draft added to common_mail_args (one place, all four helpers get it)
  • resolve_draft_method navigates users.drafts.create in the discovery doc
  • resolve_mail_method dispatcher selects between send and draft based on the flag
  • dispatch_raw_email (renamed from send_raw_email to reflect its dual role) switches the method and wraps draft metadata in the {"message": {...}} envelope required by the Drafts API
  • Threaded drafts (replies and forwards) preserve threadId in the metadata
  • After successful draft creation, a tip is printed to stderr with the users.drafts.send command (suppressed during --dry-run)

Related issues and PRs

Live testing

Tested against a real Gmail account:

# Test Result
1 +send --draft Draft created with DRAFT label
2 +reply --draft Threaded draft in correct thread
3 +send --draft --dry-run Dry-run output, no tip printed
4 +send (without --draft) Sends normally (no regression)

Test coverage

782 total tests (6 new). New tests cover:

  • build_send_metadata for all four (thread_id, draft) combinations
  • resolve_draft_method discovery doc traversal
  • Absence of threadId in draft metadata when no thread

Dry Run Output:

{
  "body": "{\"message\":{}}",
  "dry_run": true,
  "is_multipart_upload": true,
  "method": "POST",
  "query_params": [],
  "url": "https://gmail.googleapis.com/upload/gmail/v1/users/me/drafts"
}

Checklist:

  • My code follows the AGENTS.md guidelines (no generated google-* crates).
  • I have run cargo fmt --all to format the code perfectly.
  • I have run cargo clippy -- -D warnings and resolved all warnings.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have provided a Changeset file (e.g. via pnpx changeset) to document my changes.

malob added 2 commits March 18, 2026 15:33
Include original message attachments on +forward by default, matching
Gmail web behavior. Add --no-original-attachments flag to opt out
(skips file attachments but preserves inline images in HTML mode).

Preserve cid: inline images in HTML mode for both +forward and
+reply/+reply-all by building the correct multipart/related MIME
structure via mail-builder's MimePart API. Gmail's API rewrites
Content-Disposition: inline to attachment in multipart/mixed, so
explicit multipart/related is required.

In plain-text mode, inline images are not included for both forward
and reply, matching Gmail web behavior.

Key implementation details:
- Single-pass MIME payload walker replaces separate text/html extractors
- OriginalPart metadata type with lazy attachment data fetching
- Part classification uses Content-Disposition to distinguish regular
  attachments from inline images (some clients set Content-ID on both)
- Content-ID and content_type sanitized against CRLF header injection
- Size preflight before downloading original attachments
- Remote filename sanitization (not rejection) for sender-controlled names
- Walker does not recurse into hydratable parts (e.g., message/rfc822)
When --draft is set, calls users.drafts.create instead of
users.messages.send. Message construction is identical; only the
API method and metadata wrapper change. Threaded drafts (replies
and forwards) preserve threadId in the draft metadata.
@changeset-bot
Copy link

changeset-bot bot commented Mar 20, 2026

🦋 Changeset detected

Latest commit: 93997d9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@googleworkspace/cli Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@googleworkspace-bot googleworkspace-bot added area: skills area: core Core CLI parsing, commands, error handling, utilities labels Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Core CLI parsing, commands, error handling, utilities area: skills

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants