193 Commits

Author SHA1 Message Date
googleworkspace-bot e3e1e86e07 chore: release versions (#440) v0.13.2 2026-03-12 14:33:37 -06:00
Justin Poehnelt 86ea6dea32 fix(auth): validate --subscription in gmail +watch and deduplicate PUBSUB_API_BASE (#441)
* fix(auth): validate --subscription in gmail +watch and deduplicate PUBSUB_API_BASE

- Move PUBSUB_API_BASE constant to helpers/mod.rs (shared by events/subscribe and gmail/watch)
- Add validate_resource_name on --subscription in gmail +watch parse_watch_args
- Replace remaining hardcoded Pub/Sub and Gmail API URLs with constants
- Add test for --subscription path traversal rejection

Closes #408

* fix: replace remaining hardcoded Pub/Sub URLs in subscribe.rs with PUBSUB_API_BASE

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-12 14:26:36 -06:00
Eduard Tanase 3dcf818cf1 fix(auth): refresh OAuth2 tokens in long-running watch/subscribe loops (#407)
* Fix token refresh for long-running helpers

* Reuse tokens within a single loop iteration

Remove redundant access_token() calls for acknowledge requests (both
loops) and per-message Gmail fetches.  The token obtained at the start
of the iteration / function is still valid and is now reused for all
HTTP calls within the same pass.

Addresses review feedback from gemini-code-assist.

* Address review: deduplicate FakeTokenProvider, refresh gmail token per message

- Move FakeTokenProvider to auth.rs behind #[cfg(test)] so both helper
  test modules share one definition.
- Refresh the Gmail token per message inside fetch_and_output_messages
  to guard against expiry during large batches.
- Update test expectations accordingly.

* Warn instead of printing misleading 'Cleanup complete' on token failure

Move the success message inside the token-refresh guard and add an else
branch that warns about potential orphaned resources.

* Reuse single gmail token per fetch_and_output_messages call

A single history batch won't approach the token's 1-hour lifetime,
so per-message refresh is unnecessary overhead. The outer watch_pull_loop
already refreshes tokens each iteration for long-running resilience.
2026-03-12 14:14:59 -06:00
Justin Poehnelt 3393dcd797 ci: avoid scope creep in gemini code assist reviews (#437) 2026-03-12 12:43:18 -06:00
googleworkspace-bot fb27a510dd chore: release versions (#436) v0.13.1 2026-03-12 12:03:17 -06:00
Justin Poehnelt 510024f6d1 fix(auth): auto-recover from stale encrypted credentials after upgrade (#435)
* fix(auth): auto-recover from stale encrypted credentials after upgrade

When credentials.enc cannot be decrypted (e.g. after an upgrade that
changed the encryption key), automatically remove the stale file and
fall through to other credential sources (plaintext, ADC) instead of
hard-erroring. This breaks the stuck loop where logout+login couldn't
fix the issue.

Also sync .encryption_key file backup when the keyring has a valid key
but the file is missing, preventing future key loss if the keyring
becomes unavailable.

Fixes #389

* fix: log warnings instead of silently ignoring file operation errors

Address review feedback: log warnings when file removal or backup
creation fails, so users get clear feedback instead of silent failures.
Token cache cleanup now skips NotFound errors since they may not exist.

* refactor: use tokio::fs::remove_file for async consistency

Switch from std::fs::remove_file to tokio::fs::remove_file in the
async load_credentials_inner function to avoid blocking the runtime,
consistent with other file operations in the same function.

* refactor: reuse parse_credential_file and use atomic key backup

- Reuse parse_credential_file for encrypted creds instead of manual
  JSON parsing. This removes duplication and adds ServiceAccount support.
- Use save_key_file_exclusive (atomic) for key backup creation, with
  AlreadyExists race condition handling.
- Update stale comment about encrypted creds being AuthorizedUser only.

* refactor: centralize token cache filenames as constants

Extract hardcoded 'token_cache.json' and 'sa_token_cache.json' strings
into TOKEN_CACHE_FILENAME and SA_TOKEN_CACHE_FILENAME constants in
auth_commands.rs. Update all 5 reference sites in auth.rs and
auth_commands.rs to use the constants.

* refactor: unconditional key sync + parse_credential_file for all paths

- Unconditionally sync the .encryption_key file with the keyring value
  on every successful read, not just when file is missing. Prevents
  stale file backups causing decryption failures if keyring becomes
  unavailable later. Uses save_key_file (overwrite + fsync) instead
  of save_key_file_exclusive (create-only).

- Use parse_credential_file for plaintext credentials at default path,
  consistent with encrypted/ADC paths. Adds ServiceAccount support
  for credentials.json, not just AuthorizedUser.

- Updated test: keyring_backend_syncs_file_when_keyring_differs
  verifies file content is overwritten to match keyring key.

* revert: remove scope-creep changes (constants + plaintext SA support)

Move TOKEN_CACHE_FILENAME/SA_TOKEN_CACHE_FILENAME constants and
parse_credential_file for the plaintext credentials path to a
follow-up refactor PR — keep this PR focused on stale credential
auto-recovery.

* style: use tokio::fs::write in async test functions

Replace std::fs::write with tokio::fs::write().await in async test
functions for consistency with the async runtime.

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-12 11:59:46 -06:00
googleworkspace-bot f155fdf7b4 chore: release versions (#434)
* chore: release versions

* chore: regenerate skills [skip ci]

---------

Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
2026-03-12 10:16:28 -06:00
Anshul Garg e104106cd7 docs: warn about zsh ! history expansion in sheet range examples (#412)
* docs: warn about zsh \! history expansion in sheet range examples

Add a "Shell Tips" section to the gws-shared skill explaining that
zsh interprets \! inside single quotes as history expansion, which
mangles sheet ranges like 'Sheet1\!A1:B2'.

Replace single quotes with double quotes around sheet ranges
containing \! in:
- registry/recipes.yaml (source for generated recipe skills)
- src/helpers/sheets.rs (+read after_help examples)
- src/generate_skills.rs (shared skill template)
- Generated SKILL.md files for affected recipes and gws-sheets-read

Closes #268

* fix: escape double quotes in after_help string literal

The after_help string for +read uses a regular string literal, not a
raw string, so inner double quotes must be escaped with backslashes.
2026-03-12 10:15:51 -06:00
Malo Bourgon 9d937af8af feat(gmail): add --html flag for HTML email composition (#417)
Add --html flag to +send, +reply, +reply-all, and +forward, enabling
HTML email composition. When set, --body is treated as HTML content and
the Content-Type switches from text/plain to text/html.

For replies and forwards, the quoted/forwarded block matches Gmail web
UI fidelity: gmail_quote_container class, gmail_sendername structure,
mailto links in attribution and metadata, <div dir="ltr"> wrapper on
quoted content, and RFC 2822 dates reformatted to Gmail's human-friendly
style. When the original message has no HTML body, plain text is
HTML-escaped with <br> line breaks as a fallback (with a stderr
diagnostic).
2026-03-12 10:09:15 -06:00
Abhi Ram Reddy Salammagari 2df32eed1b docs: document helper commands and the + prefix convention (#427)
* docs: document helper commands and the + prefix convention

Add a "Helper Commands" section to the Advanced Usage chapter of the
README explaining:

- What the `+` prefix means and why it exists (visually distinct from
  Discovery-generated method names, no collision risk)
- How to discover helpers via `gws <service> --help`
- A full reference table of all 23 helper commands across 11 services
- Usage examples for the most common helpers (gmail, sheets, calendar,
  drive, workflow)

Fixes discoverability gap: users had no way to learn about helper
commands without reading the source code.

* fix(docs): correct +append and +upload examples per Gemini review

- gws sheets +append: flag is --spreadsheet (not --spreadsheet-id) and
  +append has no --range argument
- gws drive +upload: file path is a positional argument, not --file flag

* docs: clarify script +push is destructive (replaces, not adds)

The +push helper replaces all files in an Apps Script project.
Update description to reflect this so users understand the action
is destructive before running it.

Addresses code-review feedback.

* chore(changeset): correct helper command count to 24 across 10 services

Addresses code-review feedback noting the count was off.
2026-03-12 10:06:13 -06:00
googleworkspace-bot aacda4e634 chore: release versions (#433) v0.12.0 2026-03-12 10:04:46 -06:00
github-actions[bot] 89f24b88ed style: cargo fmt 2026-03-12 16:03:11 +00:00
Abhi Ram Reddy Salammagari 247e27a876 feat(error): add structured exit codes for scriptable error handling (#428)
* feat(error): add structured exit codes for scriptable error handling

Replace the hardcoded `std::process::exit(1)` with a type-specific exit
code derived from the GwsError variant:

  0 — success
  1 — API error      (GwsError::Api)
  2 — auth error     (GwsError::Auth)
  3 — validation     (GwsError::Validation)
  4 — discovery      (GwsError::Discovery)
  5 — internal       (GwsError::Other)

This allows shell scripts to branch on failure type without parsing the
JSON error output:

  gws drive files list ...
  case $? in
    1) echo "API error — check your params" ;;
    2) echo "Auth error — run: gws auth login" ;;
    3) echo "Bad arguments" ;;
  esac

Changes:
- Add GwsError::exit_code() mapping variants to codes
- Update main() to call std::process::exit(err.exit_code())
- Document exit codes in gws --help (print_usage)
- Document exit codes in README under new Exit Codes section
- Add 6 unit tests including a regression guard asserting all codes are distinct

* refactor(error): replace magic exit-code numbers with named constants

Add EXIT_CODE_API/AUTH/VALIDATION/DISCOVERY/OTHER associated constants
on GwsError so callers and tests reference symbolic names rather than
bare integers. Update exit_code() match arms and all tests accordingly.
The distinctness test now validates the constants array directly.

Addresses code-review feedback requesting named constants.

* refactor(error): centralize exit code help text via EXIT_CODE_DOCUMENTATION

Add a module-level EXIT_CODE_DOCUMENTATION constant — a static slice of
(code, description) pairs built from the EXIT_CODE_* constants. Replace
the hardcoded println! block in print_usage() with a loop over this slice
so the help output is always in sync with the defined constants and cannot
drift out of date.

Addresses code-review feedback requesting a single source of truth.
2026-03-12 10:02:47 -06:00
Zsombor Szabo 087066f946 fix(auth): enable native keyring backends on top of #359 (#373)
* fix(auth): enable native keyring backends

* test(auth): serialize config dir env test

* fix(auth): scope native keyring backends to desktop targets

* fix(ci): tolerate read-only gemini review tokens

* test(credential_store): cover race winner sync path
2026-03-12 09:58:52 -06:00
googleworkspace-bot 7e22a3d0e3 chore: release versions (#390) v0.11.1 2026-03-10 15:53:11 -06:00
drassi adbca874ab fix: handle array-of-arrays in CSV formatter (#288)
* fix: handle array-of-arrays in CSV formatter

The CSV formatter assumed all items were JSON objects when collecting
column names. APIs that return arrays of arrays (e.g. Sheets values)
produced empty newlines instead of data.

Add an early return path for non-object arrays that emits each inner
array's elements as CSV cells directly, mirroring the table formatter's
existing "array of non-objects" handling.

Fixes #283

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add coverage for flat scalar CSV formatting

---------

Co-authored-by: Dan Rassi <129646+drassi@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 15:26:06 -06:00
Justin Poehnelt bd5041e3da chore: Update workflow to use GOOGLEWORKSPACE_BOT_TOKEN and googleworkspace-bot identity for operations. (#388) v0.11.0 2026-03-10 14:25:45 -06:00
googleworkspace-bot e6bedc0d21 chore: release versions (#387)
* chore: release versions

* chore: regenerate skills [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-10 14:18:03 -06:00
Malo Bourgon 4d4b09f9cc feat(gmail): add recipient management flags and extract shared message builder (#362)
* feat(gmail): add --cc, --bcc, --to recipient management flags

Add --cc and --bcc to +send, --to and --bcc to +reply and +reply-all,
and --bcc to +forward. This brings all four Gmail helpers to feature
parity for basic recipient management.

Key behaviors:
- --to on reply/reply-all is additive (appends to auto-computed To)
- --remove only affects auto-computed recipients, not explicit flags
- Dedup with priority To > CC > BCC via new dedup_recipients()
- Validation deferred until after all additions and dedup
- Empty/whitespace --cc/--bcc/--to filtered to None at parse time
- BCC header included in raw message (Gmail API strips before delivery)

Also includes:
- CAUTION boxes in all four SKILL.md files
- SendConfig visibility narrowed to pub(super)
- Consistent "email address(es)" wording across clap help and SKILL.md

* refactor(gmail): extract shared MessageBuilder and fix pre-existing issues

Extract duplicated header-construction logic from send.rs, reply.rs, and
forward.rs into a shared MessageBuilder in mod.rs. This centralizes CRLF
header-injection sanitization and RFC 2047 subject encoding that were
previously inconsistent across the three paths.

Additional changes:
- Fix silent auth failure in send.rs (Err(_) swallowed all auth errors)
- Fix try_get_one error swallowing in parse_reply_args (explicit match
  on MatchesError::UnknownArgument, propagate unexpected errors)
- Extract shared helpers: build_references, parse_optional_trimmed,
  encode_header_value, sanitize_header_value
- Introduce ForwardEnvelope (analogous to ReplyEnvelope)
- Introduce ThreadingHeaders to group in_reply_to/references
2026-03-10 14:13:32 -06:00
Justin Poehnelt 5445a476bb fix(ci): check remote tags in tag-release.sh (#385)
The idempotency guard only checked local tags, but CI checkouts
don't fetch tags. Add git ls-remote fallback so re-runs of the
Release workflow skip already-pushed tags.

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-10 14:03:52 -06:00
Justin Poehnelt ad99aa9a2a ci: use bot token for /gemini review comment (#384)
Gemini Code Assist ignores comments from github-actions[bot].
Using GOOGLEWORKSPACE_BOT_TOKEN posts as googleworkspace-bot instead.

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-10 13:23:53 -06:00
googleworkspace-bot 58c412c768 chore: release versions (#351) v0.10.0 2026-03-10 12:30:44 -06:00
jpoehnelt-bot 80fd6c51ea ci: add workflow_dispatch trigger to Release (Changeset)
Allows manual re-triggering when bot-merged PRs don't fire the push event.
2026-03-10 12:28:15 -06:00
Justin Poehnelt 8d89325a8b feat(credential_store): add GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND env var (#359)
* feat(credential_store): add GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND env var

Add gogcli-style backend selection for encryption key storage:
- keyring (default): OS keyring with file fallback
- file: .encryption_key file only (Docker/CI/headless)

Never delete .encryption_key — it always serves as a durable fallback
for environments where the keyring is ephemeral. When generating new
keys with backend=keyring, save to both keyring and file.

Extracts KeyringProvider trait + resolve_key() for testability.
25 tests covering both backends and all edge cases.

Fixes #344

* chore: regenerate skills [skip ci]

* fix(credential_store): use O_EXCL for race-safe key generation

Use create_new(true) (O_EXCL on Unix, CREATE_NEW on Windows) when
generating a new encryption key file. If another process wins the
race, read their key instead. Platform-independent.

* fix(credential_store): sync winner's key into keyring after file race

When two processes race to create the encryption key file, the loser
now syncs the winner's key back into the keyring. Without this, the
keyring and file could permanently diverge.

* test(credential_store): add 9 tests covering file exclusion, env parsing, and race paths

- save_key_file_exclusive: creates new file, rejects existing
- save_key_file: overwrites existing
- ensure_key_dir: creates nested dirs
- KeyringBackend: file/FILE/invalid parsing
- Race loser: syncs winner key to keyring
- Race loser: corrupt file gets overwritten

* feat(credential_store): security and robustness hardening

1. Warn on unrecognized KEYRING_BACKEND values instead of silent default
2. fsync after key file writes for crash durability
3. Zeroize decoded key material from heap after copy
4. Warn if key file has overly permissive Unix permissions (mode & 077)
5. Log which keyring backend was selected to stderr
6. Expose keyring_backend in 'gws auth status' JSON output

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-10 11:17:35 -06:00
Malo Bourgon 5e7d1200df fix(gmail): bring +forward behavior in line with Gmail web UI (#353)
- Pass original threadId to keep forward in sender's thread
- Add In-Reply-To and References headers for RFC 5322 compliance
- Add blank line between forwarded message metadata and body
- Remove spurious closing delimiter from forwarded message block
- Update SKILL.md to remove outdated threading tip
- Add test for References chain construction

Closes #88
2026-03-09 17:47:35 -06:00
Justin Poehnelt 06aa698e23 fix(auth): format and deduplicate dynamic scope fallback (#352)
* fix(auth): fall back to Discovery docs when `-s` specifies services not in static scope lists

When `gws auth login -s chat` (or any service not in the 7 static
scope lists) is used, the static filter returns no matching scopes.

Add a dynamic fallback that detects unmatched services and fetches
their OAuth scopes from the Google Discovery API. This leverages the
existing `fetch_discovery_document` with 24h caching.

Fixes #236

* refactor: optimize find_unmatched_services and parallelize Discovery fetches

Address review feedback:
- Avoid per-service HashSet allocation in find_unmatched_services by
  collecting matched services first then computing the difference.
- Use futures_util::future::join_all to fetch Discovery docs in
  parallel instead of sequentially.

* refactor: extract map_service_to_scope_prefix to deduplicate alias mapping

Share the service-name-to-scope-prefix mapping between
scope_matches_service and find_unmatched_services via a single helper.

* fix(auth): format extract_scopes_from_doc and deduplicate dynamic scopes

- Break long method chain in extract_scopes_from_doc to pass cargo fmt
- Deduplicate dynamic scopes in augment_with_dynamic_scopes to prevent
  duplicate entries when dynamic results overlap with static ones

---------

Co-authored-by: Frank <qwer4488999@gmail.com>
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-09 17:36:49 -06:00
zerone0x 2782cf101e fix: use gmail.readonly scope in +triage to avoid metadata scope 403 (#304)
The +triage helper uses the `q` query parameter when listing messages,
but Gmail's metadata scope does not support `q` and returns 403. When a
user's OAuth token includes both gmail.metadata and gmail.modify scopes,
the API may resolve to the metadata code path and reject the query.

Switch +triage from gmail.modify to gmail.readonly, which is the
minimum scope that supports query filtering and aligns with the
read-only nature of the triage command.

Fixes #265
2026-03-09 17:08:34 -06:00
googleworkspace-bot dd7830f7fa chore: release versions (#350) 2026-03-09 16:09:37 -06:00
googleworkspace-bot cfa0984346 chore: release versions (#348) v0.9.1 2026-03-09 16:05:03 -06:00
Justin Poehnelt b0f159eea0 ci: run smoketest immediately after linux build, gate other builds on it (#349)
Split x86_64-unknown-linux-gnu out of the build matrix into a dedicated
build-linux job. The smoketest now depends only on build-linux, running
as soon as that single build completes. The remaining cross-platform
builds (macOS, Windows, aarch64-linux) depend on the smoketest, so they
are skipped entirely if the smoketest fails — saving CI minutes.

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-09 16:04:44 -06:00
Justin Poehnelt 5872dbe474 fix(credential_store): stop persisting encryption key file when keyring is available (#345)
Previously, get_or_create_key() unconditionally wrote the encryption key
to ~/.config/gws/.encryption_key on first run, even when the OS keyring
was available. This left the key material on disk as a plain file,
making credentials portable by copying the config directory.

Changes:
- Extract save_key_file() helper to deduplicate file-writing logic
- On keyring read success: delete stale .encryption_key (migration)
- On NoEntry + existing file: migrate key into keyring, then delete file
- On NoEntry + new key: try keyring first, only write file as fallback

Fixes #344

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-09 15:51:05 -06:00
Justin Poehnelt 06edf7b05f fix(ci): add missing tool input to taiki-e/install-action for cargo-llvm-cov (#347)
When actions were SHA-pinned in #341, the tool name was lost. The
original uses: taiki-e/install-action@cargo-llvm-cov passed the tool
name as the tag. After pinning to a SHA, the tool input must be
specified explicitly via with: tool: cargo-llvm-cov.

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-09 15:41:37 -06:00
Justin Poehnelt 00adb5cb23 ci: make sccache setup resilient to cache API outages (#346)
Remove RUSTC_WRAPPER from global env and set it per-job only after the
sccache-action succeeds. When GitHub's cache API is unavailable, CI now
falls through to a plain (uncached) cargo build instead of failing.

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-09 15:00:10 -06:00
Justin Poehnelt d5d77d96d0 ci: pin all actions to a sha (#341) 2026-03-09 13:52:49 -06:00
googleworkspace-bot 2b4bcbb270 chore: release versions (#330) v0.9.0 2026-03-09 13:16:24 -06:00
Justin Poehnelt 5836d1527a ci: ignore sccache errors when GitHub down (#339) 2026-03-09 12:54:05 -06:00
Justin Poehnelt f0a38ac871 docs: remove obsolete notice
Removed note about temporarily disabling non-collaborator pull requests.
2026-03-09 12:44:06 -06:00
Justin Poehnelt ba472602f6 build(ci): optimize nix and windows cache layers (#331) 2026-03-09 11:54:14 -06:00
jonameijers 08716f8bcd fix: RFC 2047 encode non-ASCII email subjects in +send helper (#322)
* fix: RFC 2047 encode non-ASCII email subjects in +send helper

* chore: add changeset for RFC 2047 fix

* fix: use standard Base64 and fold long encoded-words per RFC 2047

* fix: chunk at char boundaries to avoid splitting multi-byte UTF-8

* chore: fix changeset package

Fix garbled non-ASCII email subjects in gmail +send by RFC 2047 encoding the Subject header and adding MIME-Version/Content-Type headers.

---------

Co-authored-by: Jona Meijers <jonameijers@gmail.com>
Co-authored-by: Justin Poehnelt <justin.poehnelt@gmail.com>
2026-03-09 11:48:06 -06:00
googleworkspace-bot 5d6c5b1082 chore: Synced file(s) with googleworkspace/.github (#326)
* chore: Created local 'SECURITY.md' from remote 'SECURITY.md'

* chore: Created local '.vscode/extensions.json' from remote 'sync-files/defaults/.vscode/extensions.json'
2026-03-09 11:47:47 -06:00
Si Zengyu 7d15365518 feat(gmail): add +reply, +reply-all, and +forward helpers (#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 #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>
2026-03-09 11:47:02 -06:00
Max Headley f083eb9af0 fix: Improve auth setup project-creation error handling and retry UX (#95)
* Improve setup project creation error recovery

* Add post-setup login continuation flow

---------

Co-authored-by: mkh09353 <6936686+mkh09353@users.noreply.github.com>
2026-03-09 11:30:31 -06:00
Justin Poehnelt 6c266eb524 Merge pull request #329 from googleworkspace/fix/github-actions-token
fix(ci): use default GITHUB_TOKEN for PR reviews
2026-03-09 11:07:04 -06:00
Manoj Badam 789e7f1233 fix: use native OS certificate store for TLS (#302)
Switch reqwest from `rustls-tls` (bundled Mozilla roots via webpki-roots)
to `rustls-tls-native-roots` so the CLI trusts custom/corporate CA
certificates installed in the system trust store.

This fixes TLS handshake failures in enterprise environments that use
internal certificate authorities.
2026-03-09 11:06:48 -06:00
Justin Poehnelt 7c7310a9a9 fix(ci): use default GITHUB_TOKEN instead of bot account for PR reviews 2026-03-09 11:06:46 -06:00
googleworkspace-bot 643fb7a951 chore: release versions (#313) v0.8.1 2026-03-08 18:26:33 -06:00
github-actions[bot] 6f67da39b6 style: cargo fmt 2026-03-09 00:21:14 +00:00
Shane Huntley 4d41e52198 fix(auth): prioritize local project configuration over global ADC for quota attribution (#295)
* fix(auth): prioritize local project configuration over global ADC for quota attribution

* chore: add changeset for project ID priority fix
2026-03-08 18:20:56 -06:00
googleworkspace-bot 95bb24e9c2 chore: release versions (#277) v0.8.0 2026-03-06 18:31:47 -07:00
Steve Bazyl dd3fc9074d fix!: Remove MCP server mode (#275)
* BREAKING CHANGE: Remove MCP server mode

* Add changeset file
2026-03-06 11:33:23 -07:00