diff --git a/shortcuts/mail/helpers.go b/shortcuts/mail/helpers.go index 841bcc499..1cad77bf7 100644 --- a/shortcuts/mail/helpers.go +++ b/shortcuts/mail/helpers.go @@ -1163,6 +1163,7 @@ func buildMessageOutput(msg map[string]interface{}, html bool) map[string]interf out["date_formatted"] = normalized.DateFormatted out["message_state_text"] = normalized.MessageStateText if normalized.PriorityType != "" { + out["priority_type"] = normalized.PriorityType out["priority_type_text"] = normalized.PriorityTypeText } out["body_plain_text"] = normalized.BodyPlainText @@ -1241,11 +1242,22 @@ func buildMessageForCompose(msg map[string]interface{}, urlMap map[string]string out.MessageStateText = messageStateText(state) out.FolderID = strVal(msg["folder_id"]) out.LabelIDs = toStringList(msg["label_ids"]) + // Priority: prefer label_ids (HIGH_PRIORITY/LOW_PRIORITY), fall back to priority_type field. priorityType := strVal(msg["priority_type"]) out.PriorityType = priorityType if priorityType != "" { out.PriorityTypeText = priorityTypeText(priorityType) } + for _, label := range out.LabelIDs { + switch label { + case "HIGH_PRIORITY": + out.PriorityType = "1" + out.PriorityTypeText = "high" + case "LOW_PRIORITY": + out.PriorityType = "5" + out.PriorityTypeText = "low" + } + } if securityLevel := toSecurityLevel(msg["security_level"]); securityLevel != nil { out.SecurityLevel = securityLevel } @@ -1708,6 +1720,48 @@ func priorityTypeText(priorityType string) string { } } +// priorityFlag is the common flag definition for --priority, shared by all compose shortcuts. +var priorityFlag = common.Flag{ + Name: "priority", + Desc: "Email priority: high, normal, low. If omitted, no priority header is set.", +} + +// parsePriority parses the --priority flag value and returns the X-Cli-Priority +// header value. Returns "" if the priority should not be set (empty or "normal"). +func parsePriority(value string) (string, error) { + switch strings.ToLower(strings.TrimSpace(value)) { + case "": + return "", nil + case "high": + return "1", nil + case "normal": + return "", nil + case "low": + return "5", nil + default: + return "", fmt.Errorf("invalid --priority value %q: expected high, normal, or low", value) + } +} + +// validatePriorityFlag validates the --priority flag value in Validate, so invalid +// values are caught before Execute (and before dry-run prints an API plan). +func validatePriorityFlag(runtime *common.RuntimeContext) error { + v := runtime.Str("priority") + if v == "" { + return nil + } + _, err := parsePriority(v) + return err +} + +// applyPriority sets the X-Cli-Priority header on the EML builder if priority is non-empty. +func applyPriority(bld emlbuilder.Builder, priority string) emlbuilder.Builder { + if priority == "" { + return bld + } + return bld.Header("X-Cli-Priority", priority) +} + // parseNetAddrs converts a comma-separated address string to []net/mail.Address. // It reuses ParseMailboxList for display-name-aware parsing and deduplicates // by email address (case-insensitive), preserving the first occurrence. diff --git a/shortcuts/mail/helpers_test.go b/shortcuts/mail/helpers_test.go index b8087ab13..c2423248d 100644 --- a/shortcuts/mail/helpers_test.go +++ b/shortcuts/mail/helpers_test.go @@ -1077,3 +1077,143 @@ func TestValidateSendTime_Valid(t *testing.T) { t.Fatalf("expected nil for valid future send-time, got %v", err) } } + +func TestParsePriority(t *testing.T) { + cases := []struct { + name string + input string + want string + wantErr bool + }{ + {"empty", "", "", false}, + {"high", "high", "1", false}, + {"normal", "normal", "", false}, + {"low", "low", "5", false}, + {"case-insensitive HIGH", "HIGH", "1", false}, + {"whitespace padding", " low ", "5", false}, + {"invalid", "urgent", "", true}, + {"numeric not accepted", "1", "", true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := parsePriority(tc.input) + if tc.wantErr { + if err == nil { + t.Fatalf("parsePriority(%q): expected error, got nil", tc.input) + } + return + } + if err != nil { + t.Fatalf("parsePriority(%q): unexpected error: %v", tc.input, err) + } + if got != tc.want { + t.Errorf("parsePriority(%q) = %q, want %q", tc.input, got, tc.want) + } + }) + } +} + +func TestBuildMessageOutput_PriorityFromLabels(t *testing.T) { + cases := []struct { + name string + labels []interface{} + priorityType string + wantType string + wantText string + }{ + {"high from label", []interface{}{"UNREAD", "HIGH_PRIORITY"}, "", "1", "high"}, + {"low from label", []interface{}{"LOW_PRIORITY"}, "", "5", "low"}, + {"no priority label", []interface{}{"UNREAD"}, "", "", ""}, + {"label overrides priority_type field", []interface{}{"HIGH_PRIORITY"}, "5", "1", "high"}, + {"priority_type fallback when no label", []interface{}{"UNREAD"}, "1", "1", "high"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + msg := map[string]interface{}{ + "message_id": "m1", + "label_ids": tc.labels, + } + if tc.priorityType != "" { + msg["priority_type"] = tc.priorityType + } + out := buildMessageOutput(msg, false) + gotText, _ := out["priority_type_text"].(string) + if gotText != tc.wantText { + t.Errorf("priority_type_text = %q, want %q", gotText, tc.wantText) + } + gotType, _ := out["priority_type"].(string) + if gotType != tc.wantType { + t.Errorf("priority_type = %q, want %q", gotType, tc.wantType) + } + }) + } +} + +func TestApplyPriority(t *testing.T) { + // Empty priority: EML must not contain X-Cli-Priority header. + emptyBld := emlbuilder.New(). + From("", "sender@example.com"). + To("", "recipient@example.com"). + Subject("no priority"). + TextBody([]byte("body")) + emptyBld = applyPriority(emptyBld, "") + raw, err := emptyBld.BuildBase64URL() + if err != nil { + t.Fatalf("build EML failed: %v", err) + } + eml := decodeBase64URL(raw) + if strings.Contains(eml, "X-Cli-Priority") { + t.Errorf("expected no X-Cli-Priority header when priority is empty, got EML:\n%s", eml) + } + + // Non-empty priority: header must be present with the exact value. + highBld := emlbuilder.New(). + From("", "sender@example.com"). + To("", "recipient@example.com"). + Subject("high priority"). + TextBody([]byte("body")) + highBld = applyPriority(highBld, "1") + raw, err = highBld.BuildBase64URL() + if err != nil { + t.Fatalf("build EML failed: %v", err) + } + eml = decodeBase64URL(raw) + if !strings.Contains(eml, "X-Cli-Priority: 1") { + t.Errorf("expected X-Cli-Priority: 1 in EML, got:\n%s", eml) + } +} + +func TestValidatePriorityFlag(t *testing.T) { + makeRuntime := func(priority string) *common.RuntimeContext { + cmd := &cobra.Command{Use: "test"} + cmd.Flags().String("priority", "", "") + if priority != "" { + _ = cmd.Flags().Set("priority", priority) + } + return common.TestNewRuntimeContext(cmd, nil) + } + + cases := []struct { + name string + priority string + wantErr bool + }{ + {"empty ok", "", false}, + {"high ok", "high", false}, + {"normal ok", "normal", false}, + {"low ok", "low", false}, + {"invalid urgent", "urgent", true}, + {"invalid numeric", "1", true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := validatePriorityFlag(makeRuntime(tc.priority)) + if tc.wantErr && err == nil { + t.Errorf("validatePriorityFlag(%q): expected error, got nil", tc.priority) + } + if !tc.wantErr && err != nil { + t.Errorf("validatePriorityFlag(%q): unexpected error: %v", tc.priority, err) + } + }) + } +} diff --git a/shortcuts/mail/mail_draft_create.go b/shortcuts/mail/mail_draft_create.go index 66875808f..f5df211fa 100644 --- a/shortcuts/mail/mail_draft_create.go +++ b/shortcuts/mail/mail_draft_create.go @@ -47,6 +47,7 @@ var MailDraftCreate = common.Shortcut{ {Name: "attach", Desc: "Optional. Regular attachment file paths (relative path only). Separate multiple paths with commas. Each path must point to a readable local file."}, {Name: "inline", Desc: "Optional. Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, signatureFlag, + priorityFlag, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { input, err := parseDraftCreateInput(runtime) @@ -79,19 +80,23 @@ var MailDraftCreate = common.Shortcut{ if err := validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), runtime.Str("body")); err != nil { return err } - return nil + return validatePriorityFlag(runtime) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { input, err := parseDraftCreateInput(runtime) if err != nil { return err } + priority, err := parsePriority(runtime.Str("priority")) + if err != nil { + return err + } mailboxID := resolveComposeMailboxID(runtime) sigResult, err := resolveSignature(ctx, runtime, mailboxID, runtime.Str("signature-id"), runtime.Str("from")) if err != nil { return err } - rawEML, err := buildRawEMLForDraftCreate(runtime, input, sigResult) + rawEML, err := buildRawEMLForDraftCreate(runtime, input, sigResult, priority) if err != nil { return err } @@ -129,7 +134,7 @@ func parseDraftCreateInput(runtime *common.RuntimeContext) (draftCreateInput, er return input, nil } -func buildRawEMLForDraftCreate(runtime *common.RuntimeContext, input draftCreateInput, sigResult *signatureResult) (string, error) { +func buildRawEMLForDraftCreate(runtime *common.RuntimeContext, input draftCreateInput, sigResult *signatureResult, priority string) (string, error) { senderEmail := resolveComposeSenderEmail(runtime) if senderEmail == "" { return "", fmt.Errorf("unable to determine sender email; please specify --from explicitly") @@ -190,6 +195,7 @@ func buildRawEMLForDraftCreate(runtime *common.RuntimeContext, input draftCreate } else { bld = bld.TextBody([]byte(input.Body)) } + bld = applyPriority(bld, priority) allFilePaths := append(append(splitByComma(input.Attach), inlineSpecFilePaths(inlineSpecs)...), autoResolvedPaths...) if err := checkAttachmentSizeLimit(runtime.FileIO(), allFilePaths, 0); err != nil { return "", err diff --git a/shortcuts/mail/mail_draft_create_test.go b/shortcuts/mail/mail_draft_create_test.go index 2696e5daa..a44ea7a2e 100644 --- a/shortcuts/mail/mail_draft_create_test.go +++ b/shortcuts/mail/mail_draft_create_test.go @@ -33,7 +33,7 @@ func TestBuildRawEMLForDraftCreate_ResolvesLocalImages(t *testing.T) { Body: `

Hello

`, } - rawEML, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil) + rawEML, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil, "") if err != nil { t.Fatalf("buildRawEMLForDraftCreate() error = %v", err) } @@ -58,7 +58,7 @@ func TestBuildRawEMLForDraftCreate_NoLocalImages(t *testing.T) { Body: `

Hello world

`, } - rawEML, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil) + rawEML, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil, "") if err != nil { t.Fatalf("buildRawEMLForDraftCreate() error = %v", err) } @@ -93,7 +93,7 @@ func TestBuildRawEMLForDraftCreate_AutoResolveCountedInSizeLimit(t *testing.T) { Attach: "./big.txt", } - _, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil) + _, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil, "") if err == nil { t.Fatal("expected size limit error when auto-resolved image + attachment exceed 25MB") } @@ -113,7 +113,7 @@ func TestBuildRawEMLForDraftCreate_OrphanedInlineSpecError(t *testing.T) { Inline: `[{"cid":"orphan","file_path":"./unused.png"}]`, } - _, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil) + _, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil, "") if err == nil { t.Fatal("expected error for orphaned --inline CID not referenced in body") } @@ -133,7 +133,7 @@ func TestBuildRawEMLForDraftCreate_MissingCIDRefError(t *testing.T) { Inline: `[{"cid":"present","file_path":"./present.png"}]`, } - _, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil) + _, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil, "") if err == nil { t.Fatal("expected error for missing CID reference") } @@ -142,6 +142,40 @@ func TestBuildRawEMLForDraftCreate_MissingCIDRefError(t *testing.T) { } } +func TestBuildRawEMLForDraftCreate_WithPriority(t *testing.T) { + input := draftCreateInput{ + From: "sender@example.com", + Subject: "priority test", + Body: `

Hello

`, + } + + rawEML, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil, "1") + if err != nil { + t.Fatalf("buildRawEMLForDraftCreate() error = %v", err) + } + eml := decodeBase64URL(rawEML) + if !strings.Contains(eml, "X-Cli-Priority: 1") { + t.Errorf("expected X-Cli-Priority: 1 in EML, got:\n%s", eml) + } +} + +func TestBuildRawEMLForDraftCreate_NoPriority(t *testing.T) { + input := draftCreateInput{ + From: "sender@example.com", + Subject: "no priority", + Body: `

Hello

`, + } + + rawEML, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil, "") + if err != nil { + t.Fatalf("buildRawEMLForDraftCreate() error = %v", err) + } + eml := decodeBase64URL(rawEML) + if strings.Contains(eml, "X-Cli-Priority") { + t.Errorf("expected no X-Cli-Priority header when priority is empty, got:\n%s", eml) + } +} + func TestBuildRawEMLForDraftCreate_PlainTextSkipsResolve(t *testing.T) { chdirTemp(t) os.WriteFile("img.png", []byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, 0o644) @@ -153,7 +187,7 @@ func TestBuildRawEMLForDraftCreate_PlainTextSkipsResolve(t *testing.T) { PlainText: true, } - rawEML, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil) + rawEML, err := buildRawEMLForDraftCreate(newRuntimeWithFrom("sender@example.com"), input, nil, "") if err != nil { t.Fatalf("buildRawEMLForDraftCreate() error = %v", err) } diff --git a/shortcuts/mail/mail_draft_edit.go b/shortcuts/mail/mail_draft_edit.go index 5e34a5bf5..2b8d07329 100644 --- a/shortcuts/mail/mail_draft_edit.go +++ b/shortcuts/mail/mail_draft_edit.go @@ -33,6 +33,7 @@ var MailDraftEdit = common.Shortcut{ {Name: "set-bcc", Desc: "Replace the entire Bcc recipient list with the addresses provided here. Separate multiple addresses with commas. Display-name format is supported."}, {Name: "patch-file", Desc: "Edit entry point for body edits, incremental recipient changes, header edits, attachment changes, or inline-image changes. All body edits MUST go through --patch-file. Two body ops: set_body (full replacement including quote) and set_reply_body (replaces only user-authored content, auto-preserves quote block). Run --inspect first to check has_quoted_content, then --print-patch-template for the JSON structure. Relative path only."}, {Name: "print-patch-template", Type: "bool", Desc: "Print the JSON template and supported operations for the --patch-file flag. Recommended first step before generating a patch file. No draft read or write is performed."}, + {Name: "set-priority", Desc: "Set email priority: high, normal, low. Setting 'normal' removes any existing priority header."}, {Name: "inspect", Type: "bool", Desc: "Inspect the draft without modifying it. Returns the draft projection including subject, recipients, body summary, has_quoted_content (whether the draft contains a reply/forward quote block), attachments_summary (with part_id and cid for each attachment), and inline_summary. Run this BEFORE editing body to check has_quoted_content: if true, use set_reply_body in --patch-file to preserve the quote; if false, use set_body."}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { @@ -276,6 +277,19 @@ func buildDraftEditPatch(runtime *common.RuntimeContext) (draftpkg.Patch, error) setRecipients("cc", runtime.Str("set-cc")) setRecipients("bcc", runtime.Str("set-bcc")) + // --set-priority → inject set_header / remove_header op + if setPriority := runtime.Str("set-priority"); setPriority != "" { + headerVal, pErr := parsePriority(setPriority) + if pErr != nil { + return patch, pErr + } + if headerVal != "" { + patch.Ops = append(patch.Ops, draftpkg.PatchOp{Op: "set_header", Name: "X-Cli-Priority", Value: headerVal}) + } else { + patch.Ops = append(patch.Ops, draftpkg.PatchOp{Op: "remove_header", Name: "X-Cli-Priority"}) + } + } + if len(patch.Ops) == 0 { return patch, output.ErrValidation("at least one edit operation is required; use direct flags such as --set-subject/--set-to, or use --patch-file for body edits and other advanced operations (run --print-patch-template first)") } diff --git a/shortcuts/mail/mail_draft_edit_test.go b/shortcuts/mail/mail_draft_edit_test.go new file mode 100644 index 000000000..56be9674c --- /dev/null +++ b/shortcuts/mail/mail_draft_edit_test.go @@ -0,0 +1,92 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package mail + +import ( + "testing" + + "github.com/larksuite/cli/shortcuts/common" + "github.com/spf13/cobra" +) + +// newDraftEditRuntime creates a minimal RuntimeContext with the draft-edit +// flags used by buildDraftEditPatch. +func newDraftEditRuntime(flags map[string]string) *common.RuntimeContext { + cmd := &cobra.Command{Use: "test"} + for _, name := range []string{ + "set-subject", "set-to", "set-cc", "set-bcc", + "set-priority", "patch-file", + } { + cmd.Flags().String(name, "", "") + } + for name, val := range flags { + _ = cmd.Flags().Set(name, val) + } + return &common.RuntimeContext{Cmd: cmd} +} + +func TestBuildDraftEditPatch_SetPriorityHigh(t *testing.T) { + rt := newDraftEditRuntime(map[string]string{"set-priority": "high"}) + patch, err := buildDraftEditPatch(rt) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(patch.Ops) != 1 { + t.Fatalf("expected 1 op, got %d", len(patch.Ops)) + } + op := patch.Ops[0] + if op.Op != "set_header" { + t.Errorf("Op = %q, want set_header", op.Op) + } + if op.Name != "X-Cli-Priority" { + t.Errorf("Name = %q, want X-Cli-Priority", op.Name) + } + if op.Value != "1" { + t.Errorf("Value = %q, want 1", op.Value) + } +} + +func TestBuildDraftEditPatch_SetPriorityLow(t *testing.T) { + rt := newDraftEditRuntime(map[string]string{"set-priority": "low"}) + patch, err := buildDraftEditPatch(rt) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(patch.Ops) != 1 || patch.Ops[0].Value != "5" { + t.Fatalf("expected single set_header with value 5, got %+v", patch.Ops) + } +} + +func TestBuildDraftEditPatch_SetPriorityNormalClears(t *testing.T) { + rt := newDraftEditRuntime(map[string]string{"set-priority": "normal"}) + patch, err := buildDraftEditPatch(rt) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(patch.Ops) != 1 { + t.Fatalf("expected 1 op, got %d", len(patch.Ops)) + } + if patch.Ops[0].Op != "remove_header" || patch.Ops[0].Name != "X-Cli-Priority" { + t.Errorf("expected remove_header X-Cli-Priority, got %+v", patch.Ops[0]) + } +} + +func TestBuildDraftEditPatch_InvalidPriority(t *testing.T) { + rt := newDraftEditRuntime(map[string]string{"set-priority": "urgent"}) + if _, err := buildDraftEditPatch(rt); err == nil { + t.Fatal("expected error for invalid --set-priority value") + } +} + +func TestBuildDraftEditPatch_NoPriority(t *testing.T) { + rt := newDraftEditRuntime(map[string]string{"set-subject": "hello"}) + patch, err := buildDraftEditPatch(rt) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Only the set_subject op should be present; no priority op injected. + if len(patch.Ops) != 1 || patch.Ops[0].Op != "set_subject" { + t.Errorf("expected single set_subject op, got %+v", patch.Ops) + } +} diff --git a/shortcuts/mail/mail_forward.go b/shortcuts/mail/mail_forward.go index c7051c459..068fb157a 100644 --- a/shortcuts/mail/mail_forward.go +++ b/shortcuts/mail/mail_forward.go @@ -35,7 +35,8 @@ var MailForward = common.Shortcut{ {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {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."}, {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, - signatureFlag}, + signatureFlag, + priorityFlag}, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { messageId := runtime.Str("message-id") to := runtime.Str("to") @@ -71,7 +72,10 @@ var MailForward = common.Shortcut{ if err := validateSignatureWithPlainText(runtime.Bool("plain-text"), runtime.Str("signature-id")); err != nil { return err } - return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "") + if err := validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), ""); err != nil { + return err + } + return validatePriorityFlag(runtime) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { messageId := runtime.Str("message-id") @@ -85,6 +89,11 @@ var MailForward = common.Shortcut{ confirmSend := runtime.Bool("confirm-send") sendTime := runtime.Str("send-time") + priority, err := parsePriority(runtime.Str("priority")) + if err != nil { + return err + } + signatureID := runtime.Str("signature-id") mailboxID := resolveComposeMailboxID(runtime) sigResult, sigErr := resolveSignature(ctx, runtime, mailboxID, signatureID, runtime.Str("from")) @@ -174,6 +183,7 @@ var MailForward = common.Shortcut{ } else { bld = bld.TextBody([]byte(buildForwardedMessage(&orig, body))) } + bld = applyPriority(bld, priority) // Download original attachments and accumulate size for limit check type downloadedAtt struct { content []byte diff --git a/shortcuts/mail/mail_reply.go b/shortcuts/mail/mail_reply.go index e54022a9b..a2cec5d23 100644 --- a/shortcuts/mail/mail_reply.go +++ b/shortcuts/mail/mail_reply.go @@ -33,7 +33,8 @@ var MailReply = common.Shortcut{ {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {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."}, {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, - signatureFlag}, + signatureFlag, + priorityFlag}, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { messageId := runtime.Str("message-id") confirmSend := runtime.Bool("confirm-send") @@ -63,7 +64,10 @@ var MailReply = common.Shortcut{ if err := validateSignatureWithPlainText(runtime.Bool("plain-text"), runtime.Str("signature-id")); err != nil { return err } - return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "") + if err := validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), ""); err != nil { + return err + } + return validatePriorityFlag(runtime) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { messageId := runtime.Str("message-id") @@ -77,6 +81,11 @@ var MailReply = common.Shortcut{ confirmSend := runtime.Bool("confirm-send") sendTime := runtime.Str("send-time") + priority, err := parsePriority(runtime.Str("priority")) + if err != nil { + return err + } + inlineSpecs, err := parseInlineSpecs(inlineFlag) if err != nil { return err @@ -175,6 +184,7 @@ var MailReply = common.Shortcut{ } else { bld = bld.TextBody([]byte(bodyStr + quoted)) } + bld = applyPriority(bld, priority) allFilePaths := append(append(splitByComma(attachFlag), inlineSpecFilePaths(inlineSpecs)...), autoResolvedPaths...) if err := checkAttachmentSizeLimit(runtime.FileIO(), allFilePaths, 0); err != nil { return err diff --git a/shortcuts/mail/mail_reply_all.go b/shortcuts/mail/mail_reply_all.go index 0f719459d..ce74e118e 100644 --- a/shortcuts/mail/mail_reply_all.go +++ b/shortcuts/mail/mail_reply_all.go @@ -34,7 +34,8 @@ var MailReplyAll = common.Shortcut{ {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {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."}, {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, - signatureFlag}, + signatureFlag, + priorityFlag}, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { messageId := runtime.Str("message-id") confirmSend := runtime.Bool("confirm-send") @@ -64,7 +65,10 @@ var MailReplyAll = common.Shortcut{ if err := validateSignatureWithPlainText(runtime.Bool("plain-text"), runtime.Str("signature-id")); err != nil { return err } - return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "") + if err := validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), ""); err != nil { + return err + } + return validatePriorityFlag(runtime) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { messageId := runtime.Str("message-id") @@ -79,6 +83,11 @@ var MailReplyAll = common.Shortcut{ confirmSend := runtime.Bool("confirm-send") sendTime := runtime.Str("send-time") + priority, err := parsePriority(runtime.Str("priority")) + if err != nil { + return err + } + inlineSpecs, err := parseInlineSpecs(inlineFlag) if err != nil { return err @@ -189,6 +198,7 @@ var MailReplyAll = common.Shortcut{ } else { bld = bld.TextBody([]byte(bodyStr + quoted)) } + bld = applyPriority(bld, priority) allFilePaths := append(append(splitByComma(attachFlag), inlineSpecFilePaths(inlineSpecs)...), autoResolvedPaths...) if err := checkAttachmentSizeLimit(runtime.FileIO(), allFilePaths, 0); err != nil { return err diff --git a/shortcuts/mail/mail_send.go b/shortcuts/mail/mail_send.go index c6671cf36..d0dc3c1dc 100644 --- a/shortcuts/mail/mail_send.go +++ b/shortcuts/mail/mail_send.go @@ -33,7 +33,8 @@ var MailSend = common.Shortcut{ {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {Name: "confirm-send", Type: "bool", Desc: "Send the email immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, - signatureFlag}, + signatureFlag, + priorityFlag}, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { to := runtime.Str("to") subject := runtime.Str("subject") @@ -69,6 +70,9 @@ var MailSend = common.Shortcut{ if err := validateSignatureWithPlainText(runtime.Bool("plain-text"), runtime.Str("signature-id")); err != nil { return err } + if err := validatePriorityFlag(runtime); err != nil { + return err + } return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), runtime.Str("body")) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { @@ -85,6 +89,10 @@ var MailSend = common.Shortcut{ senderEmail := resolveComposeSenderEmail(runtime) signatureID := runtime.Str("signature-id") + priority, err := parsePriority(runtime.Str("priority")) + if err != nil { + return err + } mailboxID := resolveComposeMailboxID(runtime) sigResult, err := resolveSignature(ctx, runtime, mailboxID, signatureID, senderEmail) @@ -141,6 +149,7 @@ var MailSend = common.Shortcut{ } else { bld = bld.TextBody([]byte(body)) } + bld = applyPriority(bld, priority) allFilePaths := append(append(splitByComma(attachFlag), inlineSpecFilePaths(inlineSpecs)...), autoResolvedPaths...) if err := checkAttachmentSizeLimit(runtime.FileIO(), allFilePaths, 0); err != nil { return err diff --git a/skills/lark-mail/references/lark-mail-draft-create.md b/skills/lark-mail/references/lark-mail-draft-create.md index 22f260c1b..98e0e7885 100644 --- a/skills/lark-mail/references/lark-mail-draft-create.md +++ b/skills/lark-mail/references/lark-mail-draft-create.md @@ -52,6 +52,7 @@ lark-cli mail +draft-create --to alice@example.com --subject '测试' --body 'te | `--attach ` | 否 | 普通附件文件路径,多个用逗号分隔。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--signature-id ` | 否 | 签名 ID。附加邮箱签名到正文末尾。运行 `mail +signature` 查看可用签名。不可与 `--plain-text` 同时使用 | +| `--priority ` | 否 | 邮件优先级:`high`、`normal`、`low`。省略或 `normal` 时不设置优先级 | | `--format ` | 否 | 输出格式:`json`(默认)/ `pretty` / `table` / `ndjson` / `csv` | | `--dry-run` | 否 | 仅打印请求,不执行 | diff --git a/skills/lark-mail/references/lark-mail-draft-edit.md b/skills/lark-mail/references/lark-mail-draft-edit.md index c88d6a0b6..15fb65ef9 100644 --- a/skills/lark-mail/references/lark-mail-draft-edit.md +++ b/skills/lark-mail/references/lark-mail-draft-edit.md @@ -70,6 +70,7 @@ lark-cli mail +draft-edit --draft-id --set-subject '测试' --dry-run | `--set-to ` | 否 | 用此处提供的地址替换整个 To 收件人列表 | | `--set-cc ` | 否 | 用此处提供的地址替换整个 Cc 抄送列表 | | `--set-bcc ` | 否 | 用此处提供的地址替换整个 Bcc 密送列表 | +| `--set-priority ` | 否 | 设置邮件优先级:`high`、`normal`、`low`。设为 `normal` 会清除已有优先级 | | `--patch-file ` | 否 | 所有正文编辑、增量收件人编辑、邮件头编辑、附件变更和内嵌图片变更的入口。相对路径。先运行 `--print-patch-template` 查看 JSON 结构 | | `--print-patch-template` | 否 | 打印 `--patch-file` 的 JSON 模板和支持的操作。建议在生成补丁文件前先运行此命令。不会读取或写入草稿 | | `--inspect` | 否 | 查看草稿但不修改。返回包含 `has_quoted_content`(是否有引用区)、`attachments_summary`(含每个附件的 `part_id`、`cid`、`filename`)和 `inline_summary` 的草稿投影 | diff --git a/skills/lark-mail/references/lark-mail-forward.md b/skills/lark-mail/references/lark-mail-forward.md index d26535875..a67dfc2f4 100644 --- a/skills/lark-mail/references/lark-mail-forward.md +++ b/skills/lark-mail/references/lark-mail-forward.md @@ -67,6 +67,7 @@ lark-cli mail +forward --message-id <邮件ID> --to alice@example.com --dry-run | `--attach ` | 否 | 附件文件路径,多个用逗号分隔,追加在原邮件附件之后。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--signature-id ` | 否 | 签名 ID。附加邮箱签名到转发正文与引用块之间。运行 `mail +signature` 查看可用签名。不可与 `--plain-text` 同时使用 | +| `--priority ` | 否 | 邮件优先级:`high`、`normal`、`low`。省略或 `normal` 时不设置优先级 | | `--confirm-send` | 否 | 确认发送转发(默认只保存草稿)。仅在用户明确确认后使用 | | `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒)。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 | diff --git a/skills/lark-mail/references/lark-mail-reply-all.md b/skills/lark-mail/references/lark-mail-reply-all.md index ec3d470f5..8040af73c 100644 --- a/skills/lark-mail/references/lark-mail-reply-all.md +++ b/skills/lark-mail/references/lark-mail-reply-all.md @@ -71,6 +71,7 @@ lark-cli mail +reply-all --message-id <邮件ID> --body '测试' --dry-run | `--attach ` | 否 | 附件文件路径,多个用逗号分隔。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--signature-id ` | 否 | 签名 ID。附加邮箱签名到回复正文与引用块之间。运行 `mail +signature` 查看可用签名。不可与 `--plain-text` 同时使用 | +| `--priority ` | 否 | 邮件优先级:`high`、`normal`、`low`。省略或 `normal` 时不设置优先级 | | `--confirm-send` | 否 | 确认发送回复(默认只保存草稿)。仅在用户明确确认后使用 | | `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒)。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 | diff --git a/skills/lark-mail/references/lark-mail-reply.md b/skills/lark-mail/references/lark-mail-reply.md index 70b975582..9ffcaec23 100644 --- a/skills/lark-mail/references/lark-mail-reply.md +++ b/skills/lark-mail/references/lark-mail-reply.md @@ -74,6 +74,7 @@ lark-cli mail +reply --message-id <邮件ID> --body '

测试

' --dry-run | `--attach ` | 否 | 附件文件路径,多个用逗号分隔。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--signature-id ` | 否 | 签名 ID。附加邮箱签名到回复正文与引用块之间。运行 `mail +signature` 查看可用签名。不可与 `--plain-text` 同时使用 | +| `--priority ` | 否 | 邮件优先级:`high`、`normal`、`low`。省略或 `normal` 时不设置优先级 | | `--confirm-send` | 否 | 确认发送回复(默认只保存草稿)。仅在用户明确确认后使用 | | `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒)。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 | diff --git a/skills/lark-mail/references/lark-mail-send.md b/skills/lark-mail/references/lark-mail-send.md index a08c04809..25a33de28 100644 --- a/skills/lark-mail/references/lark-mail-send.md +++ b/skills/lark-mail/references/lark-mail-send.md @@ -71,6 +71,7 @@ lark-cli mail +send --to alice@example.com --subject '测试' --body '

test

` | 否 | 附件文件路径,多个用逗号分隔。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--signature-id ` | 否 | 签名 ID。附加邮箱签名到正文末尾。运行 `mail +signature` 查看可用签名。不可与 `--plain-text` 同时使用 | +| `--priority ` | 否 | 邮件优先级:`high`、`normal`、`low`。省略或 `normal` 时不设置优先级 | | `--confirm-send` | 否 | 确认发送邮件(默认只保存草稿)。仅在用户明确确认收件人和内容后使用 | | `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒)。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 |