Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion shortcuts/mail/draft/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,21 @@ func UpdateWithRaw(runtime *common.RuntimeContext, mailboxID, draftID, rawEML st
}

func Send(runtime *common.RuntimeContext, mailboxID, draftID string) (map[string]interface{}, error) {
return runtime.CallAPI("POST", mailboxPath(mailboxID, "drafts", draftID, "send"), nil, nil)
return SendWithTime(runtime, mailboxID, draftID, "")
}

// SendWithTime sends a draft immediately or schedules it if sendTime is set.
func SendWithTime(runtime *common.RuntimeContext, mailboxID, draftID, sendTime string) (map[string]interface{}, error) {
var body map[string]interface{}
if strings.TrimSpace(sendTime) != "" {
body = map[string]interface{}{"send_time": sendTime}
}
return runtime.CallAPI("POST", mailboxPath(mailboxID, "drafts", draftID, "send"), nil, body)
}

// CancelScheduledSend cancels a scheduled send for a message and restores the draft.
func CancelScheduledSend(runtime *common.RuntimeContext, mailboxID, messageID string) (map[string]interface{}, error) {
return runtime.CallAPI("POST", mailboxPath(mailboxID, "messages", messageID, "cancel_scheduled_send"), nil, nil)
}

func extractDraftID(data map[string]interface{}) string {
Expand Down
6 changes: 5 additions & 1 deletion shortcuts/mail/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1935,11 +1935,15 @@ func validateConfirmSendScope(runtime *common.RuntimeContext) error {

// buildSendResult builds the output map for a successful send, including
// recall tip if the backend indicates the message is recallable.
func buildSendResult(resData map[string]interface{}, mailboxID string) map[string]interface{} {
func buildSendResult(resData map[string]interface{}, mailboxID string, sendTime ...string) map[string]interface{} {
result := map[string]interface{}{
"message_id": resData["message_id"],
"thread_id": resData["thread_id"],
}
if len(sendTime) > 0 && strings.TrimSpace(sendTime[0]) != "" {
result["scheduled_send_time"] = sendTime[0]
result["scheduled_send_time_human"] = formatScheduledTimeHuman(sendTime[0])
}
if recallStatus, ok := resData["recall_status"].(string); ok && recallStatus == "available" {
messageID, _ := resData["message_id"].(string)
result["recall_available"] = true
Expand Down
60 changes: 60 additions & 0 deletions shortcuts/mail/mail_cancel_scheduled_send.go
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"
Comment on lines +49 to +56
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

Preserve the shortcut-owned IDs after merging the API payload.

draftpkg.CancelScheduledSend(...) returns the raw response map, so this loop can overwrite the message_id / mailbox_id you just seeded if the API ever returns those keys. That makes the CLI output contract unstable for callers. Merge resp first, then stamp message_id, mailbox_id, and status last.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
out := map[string]interface{}{
"message_id": messageID,
"mailbox_id": mailboxID,
}
for k, v := range resp {
out[k] = v
}
out["status"] = "canceled"
out := map[string]interface{}{}
for k, v := range resp {
out[k] = v
}
out["message_id"] = messageID
out["mailbox_id"] = mailboxID
out["status"] = "canceled"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_cancel_scheduled_send.go` around lines 49 - 56, The merge
currently seeds out with message_id and mailbox_id then copies resp into out
which can overwrite those IDs; change the merge order so you first copy all keys
from resp into out (or initialize out = resp copy) and only after merging
explicitly set out["message_id"] = messageID, out["mailbox_id"] = mailboxID and
out["status"] = "canceled" to ensure the shortcut-owned IDs (from variables
messageID, mailboxID) and status override any values returned by
draftpkg.CancelScheduledSend/resp.

runtime.Out(out, nil)
return nil
},
}
30 changes: 26 additions & 4 deletions shortcuts/mail/mail_forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
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 | 🟠 Major

Don’t tell users to rerun +forward to schedule this message.

That path creates a fresh draft instead of scheduling the one you just saved, so the returned draft_id becomes a dead end. The follow-up instruction should call user_mailbox.drafts send with send_time against the saved draft.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_forward.go` around lines 237 - 241, The tip for scheduled
sends incorrectly tells users to rerun +forward; instead update the branch that
builds tip (variables: tip, mailboxID, draftID, sendTime in mail_forward.go) so
when sendTime is non-empty it prints a command that calls user_mailbox.drafts
send against the saved draft including the send_time in the --params JSON (e.g.
use user_mailbox_id, draft_id and send_time fields) rather than instructing to
rerun +forward; keep the same overall format as the existing instant-send hint
but include send_time.

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
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 | 🟠 Major

Move scheduled-send validation ahead of draft creation.

Because the second validation happens after CreateWithRaw, a request close to the 5-minute floor can fail only after the draft is already persisted. Parse once near the top of Execute and reuse the normalized value all the way through SendWithTime.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_forward.go` around lines 249 - 256, Move the send-time
parsing/validation to the start of Execute: call
parseAndValidateSendTime(sendTime) once (when strings.TrimSpace(sendTime) != "")
and store the normalized value in a variable (e.g., validatedSendTime) before
any draft creation, then pass that same validatedSendTime into CreateWithRaw and
later into draftpkg.SendWithTime; remove any later duplicate parse/validate
calls so a failing validation occurs before persisting the draft (referencing
parseAndValidateSendTime, validatedSendTime, CreateWithRaw,
draftpkg.SendWithTime, and Execute).

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
},
Expand Down
30 changes: 26 additions & 4 deletions shortcuts/mail/mail_reply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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 {
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
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 | 🟠 Major

Schedule the saved draft instead of telling users to rerun +reply.

This tip points to a second +reply --confirm-send --send-time ... invocation, which creates a new draft and leaves the just-created one behind. Since user_mailbox.drafts send already accepts send_time, the next-step guidance should schedule draft_id directly.

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
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_reply.go` around lines 200 - 204, The current tip shown
when !confirmSend instructs the user to rerun +reply, which creates a new draft;
instead update the message to show how to schedule the just-created draft
directly using the existing send endpoint (include mailboxID and draftID and
pass send_time if sendTime is set). Modify the logic that sets tip in
mail_reply.go (variables confirmSend, tip, mailboxID, draftID, sendTime) so the
default tip is like: use lark-cli mail user_mailbox.drafts send --params
'{"user_mailbox_id":"<mailboxID>","draft_id":"<draftID>","send_time":"<sendTime>"}'
when sendTime is present, otherwise omit send_time, ensuring the message
references the draft_id produced rather than suggesting a rerun of +reply.

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
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 | 🟠 Major

Parse --send-time before creating the draft.

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 Execute and reuse the normalized value through tip generation and SendWithTime.

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
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_reply.go` around lines 212 - 219, Move parsing/validation
of the --send-time flag out of the late send path and do it once during Execute
so the normalized time is reused for tip generation and when calling
draftpkg.SendWithTime; specifically, call parseAndValidateSendTime(sendTime) in
Execute (store result in a validatedSendTime variable), pass that normalized
string into any tip-generation functions, and supply it to
draftpkg.SendWithTime(runtime, mailboxID, draftID, validatedSendTime); remove
the second validation block around draftpkg.SendWithTime so you don't create a
draft and later fail revalidating.

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
},
Expand Down
30 changes: 26 additions & 4 deletions shortcuts/mail/mail_reply_all.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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 {
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
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 | 🟠 Major

Point the user at scheduling the saved draft, not rerunning +reply-all.

Following this tip creates another draft and schedules that one instead of the draft_id you just returned. The existing draft-send API already supports send_time, so the guidance should act on the saved draft directly.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_reply_all.go` around lines 215 - 218, The tip message
(variable tip) incorrectly tells the user to rerun the command to create and
schedule a new draft when sendTime is set; instead update the branch that checks
sendTime to instruct the user to call the existing drafts send API on the saved
draft (use mailboxID and draftID) with send_time set to sendTime so the command
targets the returned draft_id (i.e., construct the tip using mailboxID, draftID
and sendTime rather than suggesting rerunning +reply-all); modify the code
around tip, sendTime, mailboxID and draftID in mail_reply_all.go accordingly.

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
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 | 🟠 Major

Avoid revalidating --send-time after CreateWithRaw.

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 sendTime before any network work that creates the draft and reuse it here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/mail/mail_reply_all.go` around lines 226 - 233, Normalize and
validate sendTime before any network call that creates the draft (i.e., before
calling CreateWithRaw) by calling parseAndValidateSendTime(sendTime) once,
storing the result in a variable (validatedSendTime), and then reuse that same
validatedSendTime when calling draftpkg.SendWithTime(runtime, mailboxID,
draftID, validatedSendTime); remove the second parsing/validation after draft
creation so CreateWithRaw and SendWithTime operate on the same pre-validated
value.

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
},
Expand Down
Loading
Loading