-
Notifications
You must be signed in to change notification settings - Fork 588
mail: support scheduled send shortcuts #511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package mail | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/larksuite/cli/internal/output" | ||
| "github.com/larksuite/cli/shortcuts/common" | ||
| draftpkg "github.com/larksuite/cli/shortcuts/mail/draft" | ||
| ) | ||
|
|
||
| var MailCancelScheduledSend = common.Shortcut{ | ||
| Service: "mail", | ||
| Command: "+cancel-scheduled-send", | ||
| Description: "Cancel a scheduled email send and restore the message as a draft.", | ||
| Risk: "write", | ||
| Scopes: []string{"mail:user_mailbox.message:send"}, | ||
| AuthTypes: []string{"user"}, | ||
| HasFormat: true, | ||
| Flags: []common.Flag{ | ||
| {Name: "message-id", Desc: "Required. Message ID of the scheduled send to cancel", Required: true}, | ||
| {Name: "mailbox", Desc: "Mailbox email address that owns the scheduled message (default: me)."}, | ||
| }, | ||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| if runtime.Str("message-id") == "" { | ||
| return output.ErrValidation("--message-id is required") | ||
| } | ||
| return nil | ||
| }, | ||
| DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||
| mailboxID := resolveComposeMailboxID(runtime) | ||
| messageID := runtime.Str("message-id") | ||
| return common.NewDryRunAPI(). | ||
| Desc("Cancel a scheduled send and restore the draft"). | ||
| POST(mailboxPath(mailboxID, "messages", messageID, "cancel_scheduled_send")) | ||
| }, | ||
| Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| mailboxID := resolveComposeMailboxID(runtime) | ||
| messageID := runtime.Str("message-id") | ||
|
|
||
| resp, err := draftpkg.CancelScheduledSend(runtime, mailboxID, messageID) | ||
| if err != nil { | ||
| return output.ErrWithHint(output.ExitAPI, "api_error", fmt.Sprintf("Failed to cancel scheduled send: %v", err), "Check the message ID and make sure the message is still scheduled for delivery.") | ||
| } | ||
|
|
||
| out := map[string]interface{}{ | ||
| "message_id": messageID, | ||
| "mailbox_id": mailboxID, | ||
| } | ||
| for k, v := range resp { | ||
| out[k] = v | ||
| } | ||
| out["status"] = "canceled" | ||
| runtime.Out(out, nil) | ||
| return nil | ||
| }, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,7 @@ import ( | |
| var MailForward = common.Shortcut{ | ||
| Service: "mail", | ||
| Command: "+forward", | ||
| Description: "Forward a message and save as draft (default). Use --confirm-send to send immediately after user confirmation. Original message block included automatically.", | ||
| Description: "Forward a message and save as draft (default). Use --confirm-send to send immediately after user confirmation, or pair --confirm-send with --send-time to schedule a future send. Original message block included automatically.", | ||
| Risk: "write", | ||
| Scopes: []string{"mail:user_mailbox.message:modify", "mail:user_mailbox.message:readonly", "mail:user_mailbox:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"}, | ||
| AuthTypes: []string{"user"}, | ||
|
|
@@ -33,12 +33,14 @@ var MailForward = common.Shortcut{ | |
| {Name: "plain-text", Type: "bool", Desc: "Force plain-text mode, ignoring all HTML auto-detection. Cannot be used with --inline."}, | ||
| {Name: "attach", Desc: "Attachment file path(s), comma-separated, appended after original attachments (relative path only)"}, | ||
| {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"<unique-id>\",\"file_path\":\"<relative-path>\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via <img src=\"cid:...\"> in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, | ||
| {Name: "send-time", Desc: "Scheduled send time in RFC3339 format. Use with --confirm-send to schedule a future send. If the timezone is omitted, UTC is assumed; the time must be at least 5 minutes in the future."}, | ||
| {Name: "confirm-send", Type: "bool", Desc: "Send the forward immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, | ||
| signatureFlag}, | ||
| DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||
| messageId := runtime.Str("message-id") | ||
| to := runtime.Str("to") | ||
| confirmSend := runtime.Bool("confirm-send") | ||
| sendTime := runtime.Str("send-time") | ||
| mailboxID := resolveComposeMailboxID(runtime) | ||
| desc := "Forward: fetch original message → resolve sender address → save as draft" | ||
| if confirmSend { | ||
|
|
@@ -52,13 +54,21 @@ var MailForward = common.Shortcut{ | |
| Body(map[string]interface{}{"raw": "<base64url-EML>", "_to": to}) | ||
| if confirmSend { | ||
| api = api.POST(mailboxPath(mailboxID, "drafts", "<draft_id>", "send")) | ||
| if sendTime != "" { | ||
| api = api.Body(map[string]interface{}{"send_time": sendTime}) | ||
| } | ||
| } | ||
| return api | ||
| }, | ||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| if err := validateConfirmSendScope(runtime); err != nil { | ||
| return err | ||
| } | ||
| if sendTimeStr := runtime.Str("send-time"); sendTimeStr != "" { | ||
| if _, err := parseAndValidateSendTime(sendTimeStr); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| if runtime.Bool("confirm-send") { | ||
| if err := validateComposeHasAtLeastOneRecipient(runtime.Str("to"), runtime.Str("cc"), runtime.Str("bcc")); err != nil { | ||
| return err | ||
|
|
@@ -79,6 +89,7 @@ var MailForward = common.Shortcut{ | |
| attachFlag := runtime.Str("attach") | ||
| inlineFlag := runtime.Str("inline") | ||
| confirmSend := runtime.Bool("confirm-send") | ||
| sendTime := runtime.Str("send-time") | ||
|
|
||
| signatureID := runtime.Str("signature-id") | ||
| mailboxID := resolveComposeMailboxID(runtime) | ||
|
|
@@ -224,18 +235,29 @@ var MailForward = common.Shortcut{ | |
| return fmt.Errorf("failed to create draft: %w", err) | ||
| } | ||
| if !confirmSend { | ||
| tip := fmt.Sprintf(`draft saved. To send: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, mailboxID, draftID) | ||
| if strings.TrimSpace(sendTime) != "" { | ||
| tip = fmt.Sprintf("draft saved. To schedule send, rerun with --confirm-send and --send-time %q", sendTime) | ||
| } | ||
|
Comment on lines
237
to
+241
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t tell users to rerun That path creates a fresh draft instead of scheduling the one you just saved, so the returned 🤖 Prompt for AI Agents |
||
| runtime.Out(map[string]interface{}{ | ||
| "draft_id": draftID, | ||
| "tip": fmt.Sprintf(`draft saved. To send: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, mailboxID, draftID), | ||
| "tip": tip, | ||
| }, nil) | ||
| hintSendDraft(runtime, mailboxID, draftID) | ||
| return nil | ||
| } | ||
| resData, err := draftpkg.Send(runtime, mailboxID, draftID) | ||
| var validatedSendTime string | ||
| if strings.TrimSpace(sendTime) != "" { | ||
| validatedSendTime, err = parseAndValidateSendTime(sendTime) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| resData, err := draftpkg.SendWithTime(runtime, mailboxID, draftID, validatedSendTime) | ||
|
Comment on lines
+249
to
+256
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move scheduled-send validation ahead of draft creation. Because the second validation happens after 🤖 Prompt for AI Agents |
||
| if err != nil { | ||
| return fmt.Errorf("failed to send forward (draft %s created but not sent): %w", draftID, err) | ||
| } | ||
| runtime.Out(buildSendResult(resData, mailboxID), nil) | ||
| runtime.Out(buildSendResult(resData, mailboxID, validatedSendTime), nil) | ||
| hintMarkAsRead(runtime, mailboxID, messageId) | ||
| return nil | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ import ( | |
| var MailReply = common.Shortcut{ | ||
| Service: "mail", | ||
| Command: "+reply", | ||
| Description: "Reply to a message and save as draft (default). Use --confirm-send to send immediately after user confirmation. Sets Re: subject, In-Reply-To, and References headers automatically.", | ||
| Description: "Reply to a message and save as draft (default). Use --confirm-send to send immediately after user confirmation, or pair --confirm-send with --send-time to schedule a future send. Sets Re: subject, In-Reply-To, and References headers automatically.", | ||
| Risk: "write", | ||
| Scopes: []string{"mail:user_mailbox.message:modify", "mail:user_mailbox.message:readonly", "mail:user_mailbox:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"}, | ||
| AuthTypes: []string{"user"}, | ||
|
|
@@ -31,11 +31,13 @@ var MailReply = common.Shortcut{ | |
| {Name: "plain-text", Type: "bool", Desc: "Force plain-text mode, ignoring all HTML auto-detection. Cannot be used with --inline."}, | ||
| {Name: "attach", Desc: "Attachment file path(s), comma-separated (relative path only)"}, | ||
| {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"<unique-id>\",\"file_path\":\"<relative-path>\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via <img src=\"cid:...\"> in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, | ||
| {Name: "send-time", Desc: "Scheduled send time in RFC3339 format. Use with --confirm-send to schedule a future send. If the timezone is omitted, UTC is assumed; the time must be at least 5 minutes in the future."}, | ||
| {Name: "confirm-send", Type: "bool", Desc: "Send the reply immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, | ||
| signatureFlag}, | ||
| DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||
| messageId := runtime.Str("message-id") | ||
| confirmSend := runtime.Bool("confirm-send") | ||
| sendTime := runtime.Str("send-time") | ||
| mailboxID := resolveComposeMailboxID(runtime) | ||
| desc := "Reply: fetch original message → resolve sender address → save as draft" | ||
| if confirmSend { | ||
|
|
@@ -49,13 +51,21 @@ var MailReply = common.Shortcut{ | |
| Body(map[string]interface{}{"raw": "<base64url-EML>"}) | ||
| if confirmSend { | ||
| api = api.POST(mailboxPath(mailboxID, "drafts", "<draft_id>", "send")) | ||
| if sendTime != "" { | ||
| api = api.Body(map[string]interface{}{"send_time": sendTime}) | ||
| } | ||
| } | ||
| return api | ||
| }, | ||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| if err := validateConfirmSendScope(runtime); err != nil { | ||
| return err | ||
| } | ||
| if sendTimeStr := runtime.Str("send-time"); sendTimeStr != "" { | ||
| if _, err := parseAndValidateSendTime(sendTimeStr); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| if err := validateSignatureWithPlainText(runtime.Bool("plain-text"), runtime.Str("signature-id")); err != nil { | ||
| return err | ||
| } | ||
|
|
@@ -71,6 +81,7 @@ var MailReply = common.Shortcut{ | |
| attachFlag := runtime.Str("attach") | ||
| inlineFlag := runtime.Str("inline") | ||
| confirmSend := runtime.Bool("confirm-send") | ||
| sendTime := runtime.Str("send-time") | ||
|
|
||
| inlineSpecs, err := parseInlineSpecs(inlineFlag) | ||
| if err != nil { | ||
|
|
@@ -187,18 +198,29 @@ var MailReply = common.Shortcut{ | |
| return fmt.Errorf("failed to create draft: %w", err) | ||
| } | ||
| if !confirmSend { | ||
| tip := fmt.Sprintf(`draft saved. To send: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, mailboxID, draftID) | ||
| if strings.TrimSpace(sendTime) != "" { | ||
| tip = fmt.Sprintf("draft saved. To schedule send, rerun with --confirm-send and --send-time %q", sendTime) | ||
| } | ||
|
Comment on lines
200
to
+204
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Schedule the saved draft instead of telling users to rerun This tip points to a second Suggested fix- tip := fmt.Sprintf(`draft saved. To send: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, mailboxID, draftID)
- if strings.TrimSpace(sendTime) != "" {
- tip = fmt.Sprintf("draft saved. To schedule send, rerun with --confirm-send and --send-time %q", sendTime)
- }
+ tip := fmt.Sprintf(`draft saved. To send: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, mailboxID, draftID)
+ if strings.TrimSpace(sendTime) != "" {
+ tip = fmt.Sprintf(
+ `draft saved. To schedule this draft: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}' --data '{"send_time":"%s"}'`,
+ mailboxID, draftID, sendTime,
+ )
+ }🤖 Prompt for AI Agents |
||
| runtime.Out(map[string]interface{}{ | ||
| "draft_id": draftID, | ||
| "tip": fmt.Sprintf(`draft saved. To send: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, mailboxID, draftID), | ||
| "tip": tip, | ||
| }, nil) | ||
| hintSendDraft(runtime, mailboxID, draftID) | ||
| return nil | ||
| } | ||
| resData, err := draftpkg.Send(runtime, mailboxID, draftID) | ||
| var validatedSendTime string | ||
| if strings.TrimSpace(sendTime) != "" { | ||
| validatedSendTime, err = parseAndValidateSendTime(sendTime) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| resData, err := draftpkg.SendWithTime(runtime, mailboxID, draftID, validatedSendTime) | ||
|
Comment on lines
+212
to
+219
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parse Revalidating here is too late: near the 5-minute boundary, the command can create the draft and then fail on the second validation pass, leaving an unsent draft behind. Parse once earlier in Suggested fix+ var validatedSendTime string
+ if strings.TrimSpace(sendTime) != "" {
+ validatedSendTime, err = parseAndValidateSendTime(sendTime)
+ if err != nil {
+ return err
+ }
+ }
+
draftID, err := draftpkg.CreateWithRaw(runtime, mailboxID, rawEML)
if err != nil {
return fmt.Errorf("failed to create draft: %w", err)
}
@@
- var validatedSendTime string
- if strings.TrimSpace(sendTime) != "" {
- validatedSendTime, err = parseAndValidateSendTime(sendTime)
- if err != nil {
- return err
- }
- }
resData, err := draftpkg.SendWithTime(runtime, mailboxID, draftID, validatedSendTime)🤖 Prompt for AI Agents |
||
| if err != nil { | ||
| return fmt.Errorf("failed to send reply (draft %s created but not sent): %w", draftID, err) | ||
| } | ||
| runtime.Out(buildSendResult(resData, mailboxID), nil) | ||
| runtime.Out(buildSendResult(resData, mailboxID, validatedSendTime), nil) | ||
| hintMarkAsRead(runtime, mailboxID, messageId) | ||
| return nil | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,7 +16,7 @@ import ( | |||||||||||||||||||||||
| var MailReplyAll = common.Shortcut{ | ||||||||||||||||||||||||
| Service: "mail", | ||||||||||||||||||||||||
| Command: "+reply-all", | ||||||||||||||||||||||||
| Description: "Reply to all recipients and save as draft (default). Use --confirm-send to send immediately after user confirmation. Includes all original To and CC automatically.", | ||||||||||||||||||||||||
| Description: "Reply to all recipients and save as draft (default). Use --confirm-send to send immediately after user confirmation, or pair --confirm-send with --send-time to schedule a future send. Includes all original To and CC automatically.", | ||||||||||||||||||||||||
| Risk: "write", | ||||||||||||||||||||||||
| Scopes: []string{"mail:user_mailbox.message:modify", "mail:user_mailbox.message:readonly", "mail:user_mailbox:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"}, | ||||||||||||||||||||||||
| AuthTypes: []string{"user"}, | ||||||||||||||||||||||||
|
|
@@ -32,11 +32,13 @@ var MailReplyAll = common.Shortcut{ | |||||||||||||||||||||||
| {Name: "plain-text", Type: "bool", Desc: "Force plain-text mode, ignoring all HTML auto-detection. Cannot be used with --inline."}, | ||||||||||||||||||||||||
| {Name: "attach", Desc: "Attachment file path(s), comma-separated (relative path only)"}, | ||||||||||||||||||||||||
| {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"<unique-id>\",\"file_path\":\"<relative-path>\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via <img src=\"cid:...\"> in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, | ||||||||||||||||||||||||
| {Name: "send-time", Desc: "Scheduled send time in RFC3339 format. Use with --confirm-send to schedule a future send. If the timezone is omitted, UTC is assumed; the time must be at least 5 minutes in the future."}, | ||||||||||||||||||||||||
| {Name: "confirm-send", Type: "bool", Desc: "Send the reply immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, | ||||||||||||||||||||||||
| signatureFlag}, | ||||||||||||||||||||||||
| DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||||||||||||||||||||||||
| messageId := runtime.Str("message-id") | ||||||||||||||||||||||||
| confirmSend := runtime.Bool("confirm-send") | ||||||||||||||||||||||||
| sendTime := runtime.Str("send-time") | ||||||||||||||||||||||||
| mailboxID := resolveComposeMailboxID(runtime) | ||||||||||||||||||||||||
| desc := "Reply-all: fetch original message (with recipients) → resolve sender address → save as draft" | ||||||||||||||||||||||||
| if confirmSend { | ||||||||||||||||||||||||
|
|
@@ -50,13 +52,21 @@ var MailReplyAll = common.Shortcut{ | |||||||||||||||||||||||
| Body(map[string]interface{}{"raw": "<base64url-EML>"}) | ||||||||||||||||||||||||
| if confirmSend { | ||||||||||||||||||||||||
| api = api.POST(mailboxPath(mailboxID, "drafts", "<draft_id>", "send")) | ||||||||||||||||||||||||
| if sendTime != "" { | ||||||||||||||||||||||||
| api = api.Body(map[string]interface{}{"send_time": sendTime}) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| return api | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||||||||||||||||||||||||
| if err := validateConfirmSendScope(runtime); err != nil { | ||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if sendTimeStr := runtime.Str("send-time"); sendTimeStr != "" { | ||||||||||||||||||||||||
| if _, err := parseAndValidateSendTime(sendTimeStr); err != nil { | ||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if err := validateSignatureWithPlainText(runtime.Bool("plain-text"), runtime.Str("signature-id")); err != nil { | ||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
@@ -73,6 +83,7 @@ var MailReplyAll = common.Shortcut{ | |||||||||||||||||||||||
| attachFlag := runtime.Str("attach") | ||||||||||||||||||||||||
| inlineFlag := runtime.Str("inline") | ||||||||||||||||||||||||
| confirmSend := runtime.Bool("confirm-send") | ||||||||||||||||||||||||
| sendTime := runtime.Str("send-time") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| inlineSpecs, err := parseInlineSpecs(inlineFlag) | ||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||
|
|
@@ -201,18 +212,29 @@ var MailReplyAll = common.Shortcut{ | |||||||||||||||||||||||
| return fmt.Errorf("failed to create draft: %w", err) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if !confirmSend { | ||||||||||||||||||||||||
| tip := fmt.Sprintf(`draft saved. To send: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, mailboxID, draftID) | ||||||||||||||||||||||||
| if strings.TrimSpace(sendTime) != "" { | ||||||||||||||||||||||||
| tip = fmt.Sprintf("draft saved. To schedule send, rerun with --confirm-send and --send-time %q", sendTime) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+215
to
+218
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Point the user at scheduling the saved draft, not rerunning Following this tip creates another draft and schedules that one instead of the Suggested fix- tip := fmt.Sprintf(`draft saved. To send: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, mailboxID, draftID)
- if strings.TrimSpace(sendTime) != "" {
- tip = fmt.Sprintf("draft saved. To schedule send, rerun with --confirm-send and --send-time %q", sendTime)
- }
+ tip := fmt.Sprintf(`draft saved. To send: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, mailboxID, draftID)
+ if strings.TrimSpace(sendTime) != "" {
+ tip = fmt.Sprintf(
+ `draft saved. To schedule this draft: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}' --data '{"send_time":"%s"}'`,
+ mailboxID, draftID, sendTime,
+ )
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| runtime.Out(map[string]interface{}{ | ||||||||||||||||||||||||
| "draft_id": draftID, | ||||||||||||||||||||||||
| "tip": fmt.Sprintf(`draft saved. To send: lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, mailboxID, draftID), | ||||||||||||||||||||||||
| "tip": tip, | ||||||||||||||||||||||||
| }, nil) | ||||||||||||||||||||||||
| hintSendDraft(runtime, mailboxID, draftID) | ||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| resData, err := draftpkg.Send(runtime, mailboxID, draftID) | ||||||||||||||||||||||||
| var validatedSendTime string | ||||||||||||||||||||||||
| if strings.TrimSpace(sendTime) != "" { | ||||||||||||||||||||||||
| validatedSendTime, err = parseAndValidateSendTime(sendTime) | ||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| resData, err := draftpkg.SendWithTime(runtime, mailboxID, draftID, validatedSendTime) | ||||||||||||||||||||||||
|
Comment on lines
+226
to
+233
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid revalidating This second parse can flip a near-boundary request from valid to invalid after the draft already exists, producing a partial-success failure mode. Normalize 🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||
| return fmt.Errorf("failed to send reply-all (draft %s created but not sent): %w", draftID, err) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| runtime.Out(buildSendResult(resData, mailboxID), nil) | ||||||||||||||||||||||||
| runtime.Out(buildSendResult(resData, mailboxID, validatedSendTime), nil) | ||||||||||||||||||||||||
| hintMarkAsRead(runtime, mailboxID, messageId) | ||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve the shortcut-owned IDs after merging the API payload.
draftpkg.CancelScheduledSend(...)returns the raw response map, so this loop can overwrite themessage_id/mailbox_idyou just seeded if the API ever returns those keys. That makes the CLI output contract unstable for callers. Mergerespfirst, then stampmessage_id,mailbox_id, andstatuslast.Suggested fix
out := map[string]interface{}{ - "message_id": messageID, - "mailbox_id": mailboxID, } for k, v := range resp { out[k] = v } + out["message_id"] = messageID + out["mailbox_id"] = mailboxID out["status"] = "canceled" runtime.Out(out, nil)📝 Committable suggestion
🤖 Prompt for AI Agents