You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(mail): add draft composition command
Add `gro mail draft` for composing Gmail drafts via the users.drafts.create
API. Drafts land in the Drafts folder for human review; the CLI never calls
drafts.send, preserving the non-destructive philosophy.
Body input is markdown by default and rendered to HTML via goldmark (table,
strikethrough, and task-list extensions enabled). Use --plain for plain text
or --html to pass raw HTML verbatim. Body comes from --body, --stdin, or
--file. Supports --to/--cc/--bcc as comma-separated address lists, --from
for send-as aliases, and --attach (repeatable) for file attachments.
MIME assembly is RFC 5322/2045 compliant: MIME-Version: 1.0, CRLF line
endings, Content-Transfer-Encoding: base64 with 76-column wrapping. Single-
part for plain bodies, multipart/mixed for attachments. The gmail package
owns MIME construction and defends the raw boundary against CR/LF header
injection independently of command-layer validation. Attachment filenames
are basenamed (filepath.Base) so local paths never leak into the message.
Coverage:
- 21 MIME builder + httptest API wiring tests in internal/gmail/drafts_test.go
- 21 command-level tests in internal/cmd/mail/draft_test.go covering flag
presence, validation negatives (missing flags, mutual exclusions, header
injection, invalid addresses), and all success paths (markdown default,
plain, html, file, stdin, attachments, JSON output)
- 18 manual integration tests against live Gmail API — all green; see
integration-tests.md
Closes#112
* fix(mail/draft): address Codex + TDD review feedback
- Add cobra.NoArgs so unexpected positional args fail loud instead of
being silently dropped (Codex M1).
- Normalise --from via parsed.Address so display-name input like
"Work <work@me.com>" reduces to the bare address before MIME
assembly, matching --to/--cc/--bcc handling (Codex M2).
- Use cmd.Flags().Changed("file") for body-source detection so an
explicit --file "" is counted alongside --body (Codex m1).
- Extend test coverage: positional-arg rejection, --from display-name
normalisation, header-injection guard across --cc/--bcc/--from,
and detectMimeType fallback branches (no-ext, unknown-ext)
(Codex + TDD).
* fix(mail/draft): address pr-review-daemon findings
Should-fix (2):
- drafts.go: replace %q with mime.FormatMediaType for multipart boundary
(%q is Go-escape, not RFC 2045 quoted-string — happened to work today
only because the boundary contains no escape-relevant chars).
- drafts.go: document the implicit header-vs-CreatePart ordering contract
so a future refactor can't silently corrupt MIME by reordering calls.
Suggestions adopted:
- Swap iota so DraftBodyPlainText is the zero value of DraftBodyKind.
Library callers constructing DraftMessage{} get the safer default.
- Print Cc/Bcc on success output (silent omission could hide misconfig).
- Flag help: "Send body as ..." → "Treat body as ..." to align with
the never-sends philosophy.
- Document display-name stripping in --to/--cc/--bcc help and README
(Alice <alice@example.com> reduces to alice@example.com).
- integration-tests.md T2: backslash-pipe inside bash $'...' doesn't
produce a renderable markdown table — switch to a heredoc setup block.
Tests strengthened:
- Pin error message content on header-injection, plain+html, and
attachment-missing tests so regressions in the validator wording can't
slip past with a still-non-nil error.
- Use prefix-match on attachment MimeType so the test stays portable
across macOS/Linux MIME databases.
- Add coverage for --to "" (distinct code path from missing --to) and
--file pointing at a missing path.
Acknowledged but not fixed (low-value):
- Static MIME fallback for minimal containers — gro ships as a local
binary, not a container.
- 25 MB pre-flight size guard — Gmail's API error is acceptable for v1.
- Multipart CRLF assertion — net/mail.ReadMessage parsing implicitly
validates RFC 5322 compliance.
- result.ID sanitization — API-controlled alphanumeric.
- Two-way body-source pair coverage — three-way "exactly one" guard is
already exercised; pairs are redundant.
* fix(mail): address daemon run 2 findings — nil/empty guards, test coverage
- buildMIME: reject empty To recipients (defense in depth at gmail boundary)
- draft cmd: nil-guard CreateDraft response
- draft cmd: --stdin uses cmd.Flags().Changed() to match --body/--file
- detectMimeType: lowercase ext for cross-platform consistency
- mock: return non-nil DraftResult stub by default
- tests: assert BodyKind in MultipleRecipients; pin 'CR or LF' in injection table
- tests: empty body, multipart HTML body, multipart base64 line length, empty To
- integration-tests.md: replace 'gro me --id' (not a real command) with hardcoded email
* fix(mail): revert --stdin counter to bool check to match dispatch
Daemon caught a regression from the previous cycle: using cmd.Flags().Changed('stdin') for the counter while the switch dispatched on the bool diverges when a user passes --stdin=false (Changed=true, bool=false), letting validation pass and falling through to an empty body. Revert to bool check on both sides.
Drafts always land in your Gmail Drafts folder for human review. The CLI never calls `drafts.send` — sending requires explicit action in Gmail.
294
+
278
295
### Calendar Commands
279
296
280
297
All Calendar commands are under `gro calendar` (or `gro cal`):
@@ -694,6 +711,34 @@ Flags:
694
711
-j, --json Output results as JSON
695
712
```
696
713
714
+
### gro mail draft
715
+
716
+
Compose a Gmail draft and save it to the Drafts folder. The CLI never calls `drafts.send`; the draft sits in Gmail for human review and explicit send.
717
+
718
+
Body input is markdown by default and rendered to HTML. Use `--plain` for plain text or `--html` for raw HTML (no rendering). Body source is one of `--body`, `--stdin`, or `--file`.
-s, --subject string Subject line (required; empty string allowed)
729
+
--body string Body content (markdown by default)
730
+
-f, --file string Read body from file
731
+
--stdin Read body from stdin
732
+
--plain Send body as plain text (no markdown rendering)
733
+
--html Send body as raw HTML (no markdown rendering)
734
+
-a, --attach strings File path to attach (repeat for multiple)
735
+
-j, --json Output result as JSON
736
+
```
737
+
738
+
`--body`, `--stdin`, and `--file` are mutually exclusive (exactly one required). `--plain` and `--html` are mutually exclusive.
739
+
740
+
Display names in `--to`/`--cc`/`--bcc` (e.g., `Alice <alice@example.com>`) are stripped; only the email address is sent. Edit the draft in Gmail to set display names.
These scenarios exercise the full path: flag parsing → MIME assembly → Gmail API `users.drafts.create` → draft visible in Gmail Drafts UI. Each test creates a draft addressed to the authenticated user; verification is by visual inspection in Gmail and by inspecting CLI stdout/exit code.
461
+
462
+
**Setup:**
463
+
```bash
464
+
make build
465
+
ME="you@example.com"# set to the authenticated Google account
466
+
echo"user under test: $ME"
467
+
```
468
+
469
+
**Cleanup (after running):** Open Gmail → Drafts → select all `T*` drafts → Discard.
470
+
471
+
### Happy Paths
472
+
473
+
| # | Test Case | Command | Expected Result |
474
+
|---|-----------|---------|-----------------|
475
+
| T1 | Plain text body |`./bin/gro mail draft --to "$ME" --subject "T1 plain" --body "hello from gro" --plain`| Exit 0. Stdout: `Draft created: r-...`. Gmail Drafts shows plain-text body, no HTML conversion. |
476
+
| T2 | Markdown rendered to HTML (default) | See setup block below for T2 (the markdown table in the body needs a real heredoc, not inline bash quoting). | Gmail Drafts shows real H1 heading, bold/italic text, bullet list, and 2-column table — rendered, not raw markdown. |
477
+
| T3 | Raw HTML body |`./bin/gro mail draft --to "$ME" --subject "T3 html" --body '<h1 style="color:tomato">Tomato</h1><p>Inline <span style="background:yellow">highlight</span>.</p>' --html`| Gmail Drafts shows tomato-colored heading and yellow highlight. Confirms raw HTML is not double-processed. |
478
+
| T4 | Body from stdin (agentic flow) |`echo $'## Status\n\n- [x] done\n- [ ] todo' \| ./bin/gro mail draft --to "$ME" --subject "T4 stdin" --stdin`| Gmail Drafts shows H2 + task list with checkboxes (TaskList extension). |
479
+
| T5 | Body from file | (see T5 setup below) | Gmail Drafts renders headings + strikethrough + numbered list. |
480
+
| T6 | Multiple recipients (To/Cc/Bcc) |`./bin/gro mail draft --to "$ME, ${ME%@*}+to2@${ME#*@}" --cc "${ME%@*}+cc@${ME#*@}" --bcc "${ME%@*}+bcc@${ME#*@}" --subject "T6 recipients" --body "x" --plain`| Gmail Drafts shows all four addresses populated. Plus-addressing routes to same inbox but Gmail records them distinctly. |
481
+
| T7 | Single attachment | (see T7 setup below) | Stdout: `Attachments: 1`, `- gro-t7.txt`. Gmail Drafts has paperclip; attachment downloadable with original content. |
./bin/gro mail draft --to "$ME" --subject "T9 path leak check" --body "attachment name should be 'quarterly.txt' only" --plain --attach /tmp/gro-secret-dir/quarterly.txt
545
+
```
546
+
547
+
### Validation / Safety
548
+
549
+
| # | Test Case | Command | Expected Result |
550
+
|---|-----------|---------|-----------------|
551
+
| T12 | Missing `--to` rejected before API call |`./bin/gro mail draft --subject "T12" --body "x" --plain; echo "exit=$?"`| Non-zero exit. Error mentions `--to is required`. No draft in Gmail. |
552
+
| T13 | Missing `--subject` rejected |`./bin/gro mail draft --to "$ME" --body "x" --plain; echo "exit=$?"`| Non-zero exit. Error mentions `--subject is required`. |
553
+
| T14 | Two body sources rejected |`./bin/gro mail draft --to "$ME" --subject "T14" --body "x" --stdin; echo "exit=$?"`| Non-zero exit. Error mentions `exactly one of`. |
| T16 | Invalid email in `--to`|`./bin/gro mail draft --to "not-an-email" --subject "T16" --body "x" --plain; echo "exit=$?"`| Non-zero exit. Error mentions `--to`. |
556
+
| T17 | CR/LF header injection rejected |`./bin/gro mail draft --to "$ME" --subject $'T17\r\nBcc: evil@x.com' --body "x" --plain; echo "exit=$?"`| Non-zero exit. Error mentions CR or LF. No draft created. |
557
+
| T18 | Missing attachment file rejected |`./bin/gro mail draft --to "$ME" --subject "T18" --body "x" --plain --attach /nope/does/not/exist; echo "exit=$?"`| Non-zero exit before API call. Error mentions the missing path. No draft in Gmail. |
558
+
559
+
### Optional: send-as alias (`--from`)
560
+
561
+
Skip if no Gmail send-as alias is configured for the test user.
562
+
563
+
| # | Test Case | Command | Expected Result |
564
+
|---|-----------|---------|-----------------|
565
+
| T19 | Valid alias |`./bin/gro mail draft --from <configured-alias> --to "$ME" --subject "T19 alias" --body "x" --plain`| Exit 0. Gmail Drafts shows the alias in From line. |
566
+
| T20 | Unconfigured alias |`./bin/gro mail draft --from "not-my-alias@example.com" --to "$ME" --subject "T20 bad alias" --body "x" --plain; echo "exit=$?"`| Non-zero exit. API error surfaced verbatim from `creating draft: ...`. |
0 commit comments