Skip to content

feat(mail): add +draft-send shortcut for batch draft sending#1017

Open
xukuncx wants to merge 1 commit into
larksuite:mainfrom
xukuncx:feat/cf467ea
Open

feat(mail): add +draft-send shortcut for batch draft sending#1017
xukuncx wants to merge 1 commit into
larksuite:mainfrom
xukuncx:feat/cf467ea

Conversation

@xukuncx
Copy link
Copy Markdown

@xukuncx xukuncx commented May 21, 2026

Generated by the harness-coding skill.

  • Branch: feat/cf467ea
  • Target: main

Sprints

ID Title Status Commit
S1 Synthesize transport contract for larksuite/cli passed 8c700ae
S2 Add mail +draft-send shortcut + 6 mail send error codes + unit tests passed 79e0530

Summary

Adds lark-cli mail +draft-send, a high-risk-write shortcut that takes one or more existing draft IDs and sends each via POST /drafts/:draft_id/send sequentially. Per-draft failures are isolated and aggregated into a structured {sent[], failed[]} output; fatal failures (auth, permission, network, mailbox quota) abort the entire batch while recoverable failures honor --stop-on-error. Also extends internal/output with six new mail-send errno constants used by the new shortcut's fatal-error classifier.

  • 3 files added: shortcuts/mail/mail_draft_send.go, shortcuts/mail/mail_draft_send_test.go, internal/output/lark_errors.go constants
  • 1 file modified: shortcuts/mail/shortcuts.go (register MailDraftSend)
  • Scopes: mail:user_mailbox.message:send only (minimal-permission)
  • AuthTypes: user; Risk="high-risk-write" triggers framework --yes gate

This MR was created autonomously. Quality gates were enforced by the repo's own pre-commit hooks.

Summary by CodeRabbit

  • New Features
    • Added +draft-send shortcut to batch-send existing drafts (max 50 per run), with aggregated JSON output showing sent and failed items, dry-run preview, confirmation gating, and --stop-on-error support.
  • Bug Fixes
    • Batch aborts when backend indicates automation-send is disabled; partial failures return a non-zero exit status.
  • Tests
    • Comprehensive test coverage for success, partial failure, fatal aborts, dry-run, validation, and flag parsing.

Review Change Stack

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

📝 Walkthrough

Walkthrough

Adds a +draft-send CLI shortcut to send multiple drafts sequentially with aggregated JSON output, fatal-vs-recoverable error handling (including new Lark error constants), dry-run support, and comprehensive tests for success, partial failures, aborts, validation, and parsing.

Changes

Draft-Send Shortcut Implementation

Layer / File(s) Summary
Error handling contracts
internal/output/lark_errors.go, shortcuts/mail/mail_draft_send.go
Adds exported Lark mail/quota/storage error constants and implements isFatalSendErr to classify send errors as fatal or recoverable.
Shortcut definition and data model
shortcuts/mail/mail_draft_send.go, shortcuts/mail/shortcuts.go
Defines the +draft-send shortcut, flags (--mailbox, required --draft-id, --stop-on-error), and the JSON envelope types (sent[], failed[]); registers MailDraftSend.
Send execution and support functions
shortcuts/mail/mail_draft_send.go
Implements executeDraftSend to validate inputs, POST each draft sequentially, detect automation_send_disable, aggregate per-draft results, return partial_failure on any failures, and provides dryRunDraftSend and helpers.
Test infrastructure and metadata assertions
shortcuts/mail/mail_draft_send_test.go
Adds metadata/contract tests and HTTP mock helpers for per-draft send stubbing and status-driven scenarios.
Core behavior integration tests
shortcuts/mail/mail_draft_send_test.go
Integration tests for full success, partial per-draft failures (partial_failure), --stop-on-error short-circuiting, fatal mailbox-not-found abort, and automation-send-disabled handling.
Input validation and edge-case tests
shortcuts/mail/mail_draft_send_test.go
Tests for cobra flag validation (required/empty draft IDs), client-side max-batch cap, confirmation gating, dry-run behavior and plan JSON, mailbox defaulting, and CSV/repeated-flag parsing.
Unit tests for helpers
shortcuts/mail/mail_draft_send_test.go
Focused unit tests for isFatalSendErr and extractAutomationDisabledReason, plus a compile-time guard reference.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Runtime
  participant MailAPI
  participant LarkBackend
  CLI->>Runtime: run +draft-send (draft IDs, mailbox, flags)
  Runtime->>MailAPI: POST /user_mailboxes/:mb/drafts/:id/send (one per draft)
  MailAPI->>LarkBackend: forward send request
  LarkBackend-->>MailAPI: response {message_id, thread_id} or {detail, automation_send_disable}
  MailAPI-->>Runtime: HTTP response
  Runtime->>Runtime: isFatalSendErr / extractAutomationDisabledReason
  Runtime-->>CLI: aggregated envelope (sent[], failed[]) or abort with ExitAPI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • larksuite/cli#438: Introduces buildDraftSendOutput to emit message_id, thread_id, and automation_send_disable in draft send responses; this PR consumes and handles those response fields.

Suggested labels

size/XL

Suggested reviewers

  • chanthuang
  • infeng

Poem

🐰 Hopping through drafts with a careful nip,
I send each note without losing grip.
Some leap through, some stumble and stall—
I gather the tales and report them all.
A rabbit's batch, neat as a quip!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly and clearly describes the main change: adding a +draft-send shortcut for batch draft sending, matching the primary addition in the changeset.
Description check ✅ Passed The PR description covers summary, changes, test plan (with checkboxes), and related issues sections aligned with the template, providing comprehensive context about the shortcut's behavior and scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added domain/mail PR touches the mail domain size/M Single-domain feat or fix with limited business impact labels May 21, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shortcuts/mail/mail_draft_send.go`:
- Around line 123-127: The loop over draftIDs trims id for validation but later
uses the original untrimmed id when building API paths; update the code so you
trim once and use the trimmed value for both validation and path construction
(e.g., assign strings.TrimSpace(id) to a local trimmedID and use trimmedID in
the validation check and when building the request path). Apply the same change
to the other loop/block referenced (lines 131-135) so all uses of draftIDs use
the trimmed value consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69681751-04ca-4e55-8af3-2a76eddeae86

📥 Commits

Reviewing files that changed from the base of the PR and between 8c700ae and 79e0530.

📒 Files selected for processing (4)
  • internal/output/lark_errors.go
  • shortcuts/mail/mail_draft_send.go
  • shortcuts/mail/mail_draft_send_test.go
  • shortcuts/mail/shortcuts.go

Comment on lines +123 to +127
for _, id := range draftIDs {
if strings.TrimSpace(id) == "" {
return output.ErrValidation("--draft-id contains empty value")
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Trim draft IDs before building API paths.

Line 124 validates TrimSpace(id) but the request path still uses untrimmed id, so whitespace-padded CSV values can slip through and hit incorrect endpoints.

Proposed fix
-	for _, id := range draftIDs {
-		if strings.TrimSpace(id) == "" {
+	normalizedDraftIDs := make([]string, 0, len(draftIDs))
+	for _, rawID := range draftIDs {
+		id := strings.TrimSpace(rawID)
+		if id == "" {
 			return output.ErrValidation("--draft-id contains empty value")
 		}
+		normalizedDraftIDs = append(normalizedDraftIDs, id)
 	}
 
-	out := batchSendOutput{MailboxID: mailboxID, Total: len(draftIDs)}
+	out := batchSendOutput{MailboxID: mailboxID, Total: len(normalizedDraftIDs)}
 	stopOnErr := rt.Bool("stop-on-error")
-	for _, id := range draftIDs {
+	for _, id := range normalizedDraftIDs {

Also applies to: 131-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/mail/mail_draft_send.go` around lines 123 - 127, The loop over
draftIDs trims id for validation but later uses the original untrimmed id when
building API paths; update the code so you trim once and use the trimmed value
for both validation and path construction (e.g., assign strings.TrimSpace(id) to
a local trimmedID and use trimmedID in the validation check and when building
the request path). Apply the same change to the other loop/block referenced
(lines 131-135) so all uses of draftIDs use the trimmed value consistently.

Add `lark-cli mail +draft-send` shortcut that takes one or more existing
draft IDs and sends each via POST /drafts/:draft_id/send sequentially.
Per-draft failures are isolated and aggregated into a structured output;
fatal failures (auth, permission, network, mailbox quota) abort the
entire batch immediately while recoverable failures honor --stop-on-error.

Also extend internal/output with six mail-send-specific errno constants
(LarkErrMailboxNotFound=4013, LarkErrMailSendQuota{User,UserExt,TenantExt},
LarkErrMailQuota, LarkErrTenantStorageLimit) consumed by isFatalSendErr.

Risk is "high-risk-write" so the framework's --yes gate applies; the
shortcut declares only the minimal mail:user_mailbox.message:send scope
to avoid asking users for permissions it does not need.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
shortcuts/mail/mail_draft_send.go (1)

123-127: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize --draft-id values before using them in API paths.

Line 124 trims only for emptiness validation, but Line 135 and Line 186 still use raw IDs. Inputs like --draft-id "d1, d2" can produce malformed draft paths due to leading spaces.

🔧 Proposed fix
-	draftIDs := rt.StrSlice("draft-id")
+	rawDraftIDs := rt.StrSlice("draft-id")
 
-	if len(draftIDs) == 0 {
+	if len(rawDraftIDs) == 0 {
 		return output.ErrValidation("--draft-id is required")
 	}
-	if len(draftIDs) > MaxBatchSendDrafts {
+	if len(rawDraftIDs) > MaxBatchSendDrafts {
 		return output.ErrValidation(
 			"too many drafts: %d > %d (split into multiple batches)",
-			len(draftIDs), MaxBatchSendDrafts)
+			len(rawDraftIDs), MaxBatchSendDrafts)
 	}
-	for _, id := range draftIDs {
-		if strings.TrimSpace(id) == "" {
+	draftIDs := make([]string, 0, len(rawDraftIDs))
+	for _, rawID := range rawDraftIDs {
+		id := strings.TrimSpace(rawID)
+		if id == "" {
 			return output.ErrValidation("--draft-id contains empty value")
 		}
+		draftIDs = append(draftIDs, id)
 	}
@@
-	draftIDs := rt.StrSlice("draft-id")
+	rawDraftIDs := rt.StrSlice("draft-id")
@@
-	for _, id := range draftIDs {
+	for _, rawID := range rawDraftIDs {
+		id := strings.TrimSpace(rawID)
 		api = api.POST(mailboxPath(mailboxID, "drafts", id, "send"))
 	}

Also applies to: 131-135, 185-187

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/mail/mail_draft_send.go` around lines 123 - 127, The code trims
draft IDs only for emptiness but later uses the original raw values (draftIDs)
when building API paths, which allows leading/trailing spaces to create
malformed paths; update the draftIDs slice by normalizing each entry (e.g.,
strings.TrimSpace) immediately after splitting/receiving it so that all
subsequent uses (the loop using id, any path construction around where draftIDs
are iterated/used) operate on the trimmed values; modify the spot where draftIDs
is produced to overwrite each element with its trimmed version (or create a new
normalized slice) and keep the existing empty-value validation against the
trimmed entries.
🧹 Nitpick comments (1)
shortcuts/mail/mail_draft_send_test.go (1)

46-79: ⚡ Quick win

Pin the exact flag count in metadata contract assertions.

You currently verify required flags exist, but accidental extra user-declared flags would still pass. Add an exact count assertion to keep this test a strict public-surface guard.

Proposed patch
 	flagByName := map[string]common.Flag{}
 	for _, fl := range MailDraftSend.Flags {
 		flagByName[fl.Name] = fl
 	}
+	if len(MailDraftSend.Flags) != 3 {
+		t.Fatalf("Flags count = %d, want 3", len(MailDraftSend.Flags))
+	}
 	mailbox, ok := flagByName["mailbox"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/mail/mail_draft_send_test.go` around lines 46 - 79, The test
currently checks presence and properties of individual flags but doesn't fail if
extra flags are added; update the test to assert the exact flag count to lock
the public surface by adding a single assertion that the number of flags on
MailDraftSend (e.g., len(MailDraftSend.Flags) or len(flagByName)) equals 3 (the
expected flags "mailbox", "draft-id", "stop-on-error"), so any accidental
additional flags cause the test to fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@shortcuts/mail/mail_draft_send.go`:
- Around line 123-127: The code trims draft IDs only for emptiness but later
uses the original raw values (draftIDs) when building API paths, which allows
leading/trailing spaces to create malformed paths; update the draftIDs slice by
normalizing each entry (e.g., strings.TrimSpace) immediately after
splitting/receiving it so that all subsequent uses (the loop using id, any path
construction around where draftIDs are iterated/used) operate on the trimmed
values; modify the spot where draftIDs is produced to overwrite each element
with its trimmed version (or create a new normalized slice) and keep the
existing empty-value validation against the trimmed entries.

---

Nitpick comments:
In `@shortcuts/mail/mail_draft_send_test.go`:
- Around line 46-79: The test currently checks presence and properties of
individual flags but doesn't fail if extra flags are added; update the test to
assert the exact flag count to lock the public surface by adding a single
assertion that the number of flags on MailDraftSend (e.g.,
len(MailDraftSend.Flags) or len(flagByName)) equals 3 (the expected flags
"mailbox", "draft-id", "stop-on-error"), so any accidental additional flags
cause the test to fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c0d8f7e3-ed2f-4ea5-adac-20f4de9a3976

📥 Commits

Reviewing files that changed from the base of the PR and between 79e0530 and cd9f473.

📒 Files selected for processing (4)
  • internal/output/lark_errors.go
  • shortcuts/mail/mail_draft_send.go
  • shortcuts/mail/mail_draft_send_test.go
  • shortcuts/mail/shortcuts.go
✅ Files skipped from review due to trivial changes (2)
  • shortcuts/mail/shortcuts.go
  • internal/output/lark_errors.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/mail PR touches the mail domain size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants