Skip to content

feat(gmail): add +reply, +reply-all, and +forward helpers#105

Merged
jpoehnelt merged 21 commits intogoogleworkspace:mainfrom
HeroSizy:feat/gmail-reply-forward
Mar 9, 2026
Merged

feat(gmail): add +reply, +reply-all, and +forward helpers#105
jpoehnelt merged 21 commits intogoogleworkspace:mainfrom
HeroSizy:feat/gmail-reply-forward

Conversation

@HeroSizy
Copy link
Copy Markdown
Contributor

@HeroSizy HeroSizy commented Mar 5, 2026

Description

Closes #88 — adds first-class reply and forward support to the Gmail CLI helpers.

  • +reply — Reply to a message by ID. Automatically fetches the original message, sets In-Reply-To, References, and threadId headers, quotes the original, and sends via users.messages.send.
  • +reply-all — Same as +reply but addresses all original To/CC recipients. Supports --remove to drop recipients and --cc to add new ones.
  • +forward — Forward a message to new recipients with a standard forwarded-message block (From, Date, Subject, To, Cc). Supports optional --body for a note above the forwarded content.

All three commands support --dry-run for safe previewing (works without auth credentials).

Address handling details:

  • Prefers Reply-To over From when selecting reply recipients (mailing lists, support systems)
  • Parses multi-address Reply-To headers (e.g., list@example.com, owner@example.com) and deduplicates CC against the full set
  • Preserves repeated Gmail Reply-To, To, and Cc headers by concatenating values in order instead of overwriting earlier entries
  • Uses RFC 5322-aware mailbox list parsing — commas inside quoted display names (e.g., "Doe, John" <john@example.com>) are handled correctly
  • Uses exact normalized email extraction (not substring matching) for --remove filtering and sender exclusion — e.g., removing ann@example.com does not affect joann@example.com
  • Case-insensitive email comparison throughout

Known limitation: +forward currently forwards the message text/snippet only. Full MIME forwarding with attachments will be addressed in a follow-up PR (ref #88).

New files

File Purpose
src/helpers/gmail/reply.rs +reply and +reply-all logic, message metadata fetching, RFC 2822 header construction
src/helpers/gmail/forward.rs +forward logic, forwarded message formatting

Modified files

File Change
src/helpers/gmail/mod.rs Register new modules, add shared Gmail reply/forward helpers, and update subcommand dispatch.
src/helpers/gmail/send.rs Reuse shared Gmail send-method resolution.
.changeset/gmail-reply-forward.md Record the minor feature release.

Dry Run Output:

+reply:

$ gws gmail +reply --message-id 18f1a2b3c4d --body "Thanks, got it!" --dry-run
{
  "body": {
    "raw": "VG86IHNlbmRlckBleGFtcGxlLmNvbQ0KU3ViamVjdDogUmU6IE9yaWdpbmFsIHN1YmplY3QNCkluLVJlcGx5LVRvOiA8MThmMWEyYjNjNGRAZXhhbXBsZS5jb20-DQpSZWZlcmVuY2VzOiA8MThmMWEyYjNjNGRAZXhhbXBsZS5jb20-DQoNClRoYW5rcywgZ290IGl0IQ0KDQpPbiBUaHUsIDEgSmFuIDIwMjYgMDA6MDA6MDAgKzAwMDAsIHNlbmRlckBleGFtcGxlLmNvbSB3cm90ZToKPiBPcmlnaW5hbCBtZXNzYWdlIGJvZHk=",
    "threadId": "thread-18f1a2b3c4d"
  },
  "dry_run": true,
  "is_multipart_upload": false,
  "method": "POST",
  "query_params": {},
  "url": "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
}

+reply-all:

$ gws gmail +reply-all --message-id 18f1a2b3c4d --body "Sounds good!" --cc eve@example.com --remove bob@example.com --dry-run
{
  "body": {
    "raw": "VG86IHNlbmRlckBleGFtcGxlLmNvbQ0KU3ViamVjdDogUmU6IE9yaWdpbmFsIHN1YmplY3QNCkluLVJlcGx5LVRvOiA8MThmMWEyYjNjNGRAZXhhbXBsZS5jb20-DQpSZWZlcmVuY2VzOiA8MThmMWEyYjNjNGRAZXhhbXBsZS5jb20-DQpDYzogeW91QGV4YW1wbGUuY29tLCBldmVAZXhhbXBsZS5jb20NCg0KU291bmRzIGdvb2QhDQoNCk9uIFRodSwgMSBKYW4gMjAyNiAwMDowMDowMCArMDAwMCwgc2VuZGVyQGV4YW1wbGUuY29tIHdyb3RlOgo-IE9yaWdpbmFsIG1lc3NhZ2UgYm9keQ==",
    "threadId": "thread-18f1a2b3c4d"
  },
  "dry_run": true,
  "is_multipart_upload": false,
  "method": "POST",
  "query_params": {},
  "url": "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
}

+forward:

$ gws gmail +forward --message-id 18f1a2b3c4d --to dave@example.com --body "FYI see below" --cc carol@example.com --dry-run
{
  "body": {
    "raw": "VG86IGRhdmVAZXhhbXBsZS5jb20NClN1YmplY3Q6IEZ3ZDogT3JpZ2luYWwgc3ViamVjdA0KQ2M6IGNhcm9sQGV4YW1wbGUuY29tDQoNCkZZSSBzZWUgYmVsb3cNCg0KLS0tLS0tLS0tLSBGb3J3YXJkZWQgbWVzc2FnZSAtLS0tLS0tLS0KRnJvbTogc2VuZGVyQGV4YW1wbGUuY29tCkRhdGU6IFRodSwgMSBKYW4gMjAyNiAwMDowMDowMCArMDAwMApTdWJqZWN0OiBPcmlnaW5hbCBzdWJqZWN0ClRvOiB5b3VAZXhhbXBsZS5jb20KT3JpZ2luYWwgbWVzc2FnZSBib2R5Ci0tLS0tLS0tLS0=",
    "threadId": "thread-18f1a2b3c4d"
  },
  "dry_run": true,
  "is_multipart_upload": false,
  "method": "POST",
  "query_params": {},
  "url": "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
}

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.

Note: This PR was developed with AI assistance (Claude). All code has been reviewed by the author.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 5, 2026

🦋 Changeset detected

Latest commit: 9b53621

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

@google-cla
Copy link
Copy Markdown

google-cla bot commented Mar 5, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@jpoehnelt
Copy link
Copy Markdown
Member

How does this compare to the request in #88?

Copy link
Copy Markdown
Member

@jpoehnelt jpoehnelt left a comment

Choose a reason for hiding this comment

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

Code duplication between reply.rs and forward.rs
Missing encode_path_segment() on the message-id in URL construction

@HeroSizy HeroSizy force-pushed the feat/gmail-reply-forward branch 2 times, most recently from a883182 to e6f388e Compare March 5, 2026 10:15
@HeroSizy
Copy link
Copy Markdown
Contributor Author

HeroSizy commented Mar 5, 2026

How does this compare to the request in #88?

This PR is a direct implementation of the feature request in #88. It adds +reply, +reply-all, and +forward as first-class helpers, covering most of the asks from that issue:

  • Reply to a message by ID with automatic threading (In-Reply-To, References, threadId)
  • Reply-all with proper recipient dedup
  • Forward with the original message quoted
  • --cc and --remove flags for adding/dropping recipients without reconstructing the list

One thing not yet covered: attachment forwarding. The current +forward quotes the original message body but doesn't carry over attachments. That could be a follow-up if this lands — happy to open a separate issue to track it.

@HeroSizy HeroSizy force-pushed the feat/gmail-reply-forward branch from 84ebbfe to 3ec8332 Compare March 5, 2026 11:39
@HeroSizy
Copy link
Copy Markdown
Contributor Author

HeroSizy commented Mar 5, 2026

Code duplication between reply.rs and forward.rs Missing encode_path_segment() on the message-id in URL construction

@jpoehnelt

Thanks for the review! Both points are fixed now — forward.rs reuses OriginalMessage, fetch_message_metadata(), and send_raw_email() from reply.rs, and the message ID goes through encode_path_segment() in the URL construction.

Sorry about the initial state — I had an AI agent push before I'd properly reviewed. Everything's been cleaned up since.

A couple of questions:

  1. forward.rs currently reaches into reply.rs via super::reply:: for shared types. Would you prefer those extracted into a common.rs within the gmail module?

  2. We've been patching the hand-rolled mailbox parser for edge cases (escaped quotes, commas in display names, etc.) and it feels like a losing battle. Would you be open to bringing in an RFC 5322-compliant address parsing crate so we can drop the custom code entirely?

@HeroSizy HeroSizy requested a review from jpoehnelt March 5, 2026 14:33
@jpoehnelt jpoehnelt added area: skills cla: no This human has *not* signed the Contributor License Agreement. complexity: high Large or complex change, careful review needed labels Mar 5, 2026
Copy link
Copy Markdown
Member

@jpoehnelt jpoehnelt left a comment

Choose a reason for hiding this comment

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

.

@jpoehnelt
Copy link
Copy Markdown
Member

Nice work addressing the prior feedback — dedup and encode_path_segment() look good. A few things to address:

  • Query params in URL stringfetch_message_metadata manually interpolates ?format=metadata&metadataHeaders=... into the URL. Per AGENTS.md, use reqwest .query() instead
  • No From header — replies/forwards omit From:. Gmail infers it, but this breaks with aliases/send-as. Consider a --from flag or document the limitation
  • Missing MIME headers — raw messages lack Content-Type: text/plain; charset=utf-8 and MIME-Version: 1.0, which can cause encoding issues with non-ASCII text
  • VisibilityReplyConfig and ForwardConfig are pub but only used within helpers::gmail; should be pub(super)
  • Mailbox parser — agree with your suggestion to replace the hand-rolled split_mailbox_list with an RFC 5322 crate (mailparse etc.) in a follow-up
  • CLAcla: no label is present, needs signing before merge

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 6, 2026

Codecov Report

❌ Patch coverage is 77.83559% with 213 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.73%. Comparing base (f6d74b0) to head (3ec8332).
⚠️ Report is 36 commits behind head on main.

Files with missing lines Patch % Lines
src/helpers/gmail/reply.rs 79.42% 137 Missing ⚠️
src/helpers/gmail/mod.rs 64.00% 54 Missing ⚠️
src/helpers/gmail/forward.rs 84.82% 22 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #105      +/-   ##
==========================================
+ Coverage   55.19%   56.73%   +1.54%     
==========================================
  Files          38       40       +2     
  Lines       13166    14127     +961     
==========================================
+ Hits         7267     8015     +748     
- Misses       5899     6112     +213     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@HeroSizy HeroSizy force-pushed the feat/gmail-reply-forward branch from 3ec8332 to a9302c6 Compare March 6, 2026 07:43
@google-cla google-cla bot added cla: yes This human has signed the Contributor License Agreement. and removed cla: no This human has *not* signed the Contributor License Agreement. labels Mar 6, 2026
@googleworkspace-bot googleworkspace-bot added area: core Core CLI parsing, commands, error handling, utilities and removed area: skills labels Mar 6, 2026
@googleworkspace-bot
Copy link
Copy Markdown
Collaborator

/gemini review

@google-cla google-cla bot added cla: no This human has *not* signed the Contributor License Agreement. and removed cla: yes This human has signed the Contributor License Agreement. labels Mar 6, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces +reply, +reply-all, and +forward helper commands for the Gmail CLI, which is a great feature. The implementation is mostly solid, with good test coverage and documentation. However, I've identified a few areas for improvement concerning code structure and correctness. Specifically, there's a bug in handling repeated email headers, and some refactoring is needed to resolve circular dependencies and improve module organization. Addressing these points will enhance the maintainability and robustness of the new helpers.

@HeroSizy HeroSizy force-pushed the feat/gmail-reply-forward branch from d0679f8 to 2b37938 Compare March 6, 2026 08:07
@google-cla google-cla bot added cla: yes This human has signed the Contributor License Agreement. cla: no This human has *not* signed the Contributor License Agreement. and removed cla: no This human has *not* signed the Contributor License Agreement. labels Mar 6, 2026
@HeroSizy HeroSizy force-pushed the feat/gmail-reply-forward branch from 8eba0b0 to b5f2988 Compare March 7, 2026 01:59
@google-cla google-cla bot added cla: yes This human has signed the Contributor License Agreement. and removed cla: no This human has *not* signed the Contributor License Agreement. labels Mar 7, 2026
@HeroSizy
Copy link
Copy Markdown
Contributor Author

HeroSizy commented Mar 7, 2026

can you rebase this and/or push another commit to trigger the github actions

@jpoehnelt Done, rebased

@google-cla google-cla bot added cla: no This human has *not* signed the Contributor License Agreement. and removed cla: yes This human has signed the Contributor License Agreement. labels Mar 7, 2026
@googleworkspace-bot
Copy link
Copy Markdown
Collaborator

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces +reply, +reply-all, and +forward helper commands for Gmail, which is a great enhancement. The implementation is thorough, with robust handling of email headers, recipient logic, and comprehensive tests. I've found one critical issue related to parsing email headers that could lead to silently dropping recipients if the Gmail API returns multiple headers for To, Cc, or Reply-To. My review includes a suggestion to fix this.

@HeroSizy HeroSizy force-pushed the feat/gmail-reply-forward branch from 0c55a84 to 14b9487 Compare March 7, 2026 04:55
@google-cla google-cla bot added cla: yes This human has signed the Contributor License Agreement. and removed cla: no This human has *not* signed the Contributor License Agreement. labels Mar 7, 2026
@google-cla google-cla bot added cla: no This human has *not* signed the Contributor License Agreement. and removed cla: yes This human has signed the Contributor License Agreement. labels Mar 7, 2026
@googleworkspace-bot
Copy link
Copy Markdown
Collaborator

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces valuable +reply, +reply-all, and +forward helper commands for the Gmail CLI, significantly improving its usability for common email workflows. The implementation is robust, with thorough handling of email headers, recipient logic, and multipart message parsing. The addition of shared helper functions in src/helpers/gmail/mod.rs is a good architectural choice. I have one suggestion to further improve maintainability by applying one of the new helpers to the existing +send command.

@HeroSizy
Copy link
Copy Markdown
Contributor Author

HeroSizy commented Mar 9, 2026

Hi @jpoehnelt, would you mind taking a look and giving the pull request another review?

Copy link
Copy Markdown
Member

@jpoehnelt jpoehnelt left a comment

Choose a reason for hiding this comment

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

Excellent work on refactoring the shared logic and ensuring the path strings are properly encoded. Adding the robust multipart/header extractors resolves the edge cases raised previously. The CLA failure is a known false positive related to our CI token configuration. Approving this for merge!

@jpoehnelt jpoehnelt merged commit 7d15365 into googleworkspace:main Mar 9, 2026
12 of 14 checks passed
@malob
Copy link
Copy Markdown
Contributor

malob commented Mar 9, 2026

Amazing! Thanks so much :)

shigechika pushed a commit to shigechika/gws-cli that referenced this pull request Mar 20, 2026
…space#105)

* feat(gmail): add +reply, +reply-all, and +forward helper commands

Add first-class reply and forward support to the Gmail helpers,
addressing the gap described in googleworkspace#88. These commands handle the
complex RFC 2822 threading mechanics (In-Reply-To, References,
threadId) that agents and CLI users struggle with today.

New commands:
- +reply: reply to a message with automatic threading
- +reply-all: reply to all recipients with --remove/--cc support
- +forward: forward a message with quoted original content

* fix(gmail): encode message_id in URL path and fix auth signature

- Use crate::validate::encode_path_segment() on message_id in
  fetch_message_metadata URL construction per AGENTS.md rules
- Update auth::get_token calls to pass None for the new account
  parameter added on main

* refactor(gmail): extract send_raw_email and deduplicate handlers

- Add send_raw_email() to mod.rs: shared encode→json→auth→execute
  pattern for sending raw RFC 2822 messages via users.messages.send
- Simplify handle_reply: delegate send logic to send_raw_email
- Simplify handle_forward: delegate send logic to send_raw_email

Addresses code duplication feedback from PR review.

* fix(gmail): register --dry-run flag on reply/forward commands

The handlers read matches.get_flag("dry-run") but the flag was missing
from the clap command definitions, so it always returned false. Now
dry-run works for +reply, +reply-all, and +forward.

* chore: add changeset for gmail reply/forward feature

* style: apply cargo fmt formatting

* fix(gmail): register --dry-run flag on +send command

Same class of bug fixed for +reply/+reply-all/+forward — the handler
reads matches.get_flag("dry-run") but the arg was not registered.

* fix(gmail): honor Reply-To header and use exact address matching

- Prefer Reply-To over From when selecting reply recipients, fixing
  incorrect routing for mailing lists and support systems
- Use exact email address comparison instead of substring matching
  for --remove filtering and sender deduplication, preventing
  unintended recipient removal (e.g. ann@ no longer drops joann@)

* test(gmail): add comprehensive coverage for reply address handling

- extract_email: malformed input (no closing bracket), empty string,
  whitespace-only
- build_reply_all_recipients: display-name sender exclusion,
  --remove with display name, extra --cc, CC becomes None when all
  filtered, case-insensitive sender exclusion

* Improves reply-all recipient deduplication

Corrects how `build_reply_all_recipients` handles multi-address `Reply-To` headers.
Previously, only the first address from `Reply-To` was used for deduplication, leading to potential redundancy by including those addresses in the `Cc` field.
The updated logic now parses all addresses in `Reply-To`, ensuring they are fully moved to the `To` field and properly excluded from `Cc`.

* style(gmail): add missing Apache 2.0 copyright headers

reply.rs and forward.rs were missing the copyright header that all
other source files in the repo include.

* fix(gmail): use try_get_one for optional --remove arg in +reply

parse_reply_args used get_one("remove") which panics when called
from +reply (which does not register --remove). Switch to
try_get_one to safely return None for unregistered args.

* feat(gmail): support --dry-run without auth for reply/forward commands

Skip auth and message fetch when --dry-run is set by using placeholder
OriginalMessage data. This lets users preview the request structure
without needing credentials.

* fix(gmail): use RFC-aware mailbox list parsing for recipient splitting

Replace naive comma-split with split_mailbox_list that respects
quoted strings, so display names containing commas like
"Doe, John" <john@example.com> are handled correctly in reply-all
recipient parsing, deduplication, and --remove filtering.

* fix(gmail): handle escaped quotes in mailbox list splitting

split_mailbox_list toggled quote state on every `"` without accounting
for backslash-escaped quotes (`\"`), causing display names like
`"Doe \"JD, Sr\""` to split incorrectly at interior commas.

Track `prev_backslash` so `\"` inside quoted strings is treated as a
literal quote character rather than a delimiter toggle. Double
backslashes (`\\`) are handled correctly as well.

* fix(gmail): address PR review feedback for reply/forward helpers

- Use reqwest .query() for metadata params per AGENTS.md convention
- Add MIME-Version and Content-Type headers to raw messages
- Add --from flag to +reply, +reply-all, +forward for send-as/alias
- Narrow ReplyConfig/ForwardConfig visibility to pub(super)
- Refactor create_reply_raw_message args into ReplyEnvelope struct

* fix(gmail): address review feedback for reply/forward helpers

- Exclude authenticated user's own email from reply-all CC by
  fetching user profile via Gmail API
- Use format=full to extract full plain-text body instead of
  truncated snippet for quoting and forwarding
- Deduplicate CC addresses using a HashSet
- Reuse auth token from message fetch in send_raw_email to
  eliminate double auth round-trip
- Propagate auth errors in send_raw_email instead of silently
  falling back to unauthenticated requests
- Use consistent CRLF line endings in quoted and forwarded
  message bodies per RFC 2822

* fix(gmail): Gmail reply and forward helpers

* fix(gmail): refactor shared reply-forward helpers

* Preserve repeated Gmail address headers

* chore: regenerate skills [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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 cla: no This human has *not* signed the Contributor License Agreement. complexity: high Large or complex change, careful review needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: first-class reply and forward support for Gmail

4 participants