Skip to content
Open
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
118 changes: 118 additions & 0 deletions shortcuts/mail/mail_signature_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package mail

import (
"context"
"fmt"
"io"
"strings"

"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/shortcuts/common"
"github.com/larksuite/cli/shortcuts/mail/signature"
)

var MailSignatureCreate = common.Shortcut{
Service: "mail",
Command: "+signature-create",
Description: "Create a personal USER mail signature. HTML content is preserved; plain text is wrapped for email rendering. Local image paths are not uploaded.",
Risk: "write",
Scopes: []string{"mail:user_mailbox.message:modify", "mail:user_mailbox:readonly"},
AuthTypes: []string{"user"},
HasFormat: true,
Flags: []common.Flag{
{Name: "mailbox", Desc: "Mailbox address that owns the signature (default: me)."},
{Name: "name", Desc: "Required. Signature name (≤100 chars).", Required: true},
{Name: "content", Desc: "Signature content. HTML is preserved; plain text is wrapped for email rendering."},
{Name: "content-file", Desc: "Relative file path for signature content. Mutually exclusive with --content."},
{Name: "device", Default: "pc", Desc: "Signature device: pc or mobile."},
{Name: "images-json", Desc: "JSON array of signature image metadata. cid values must match <img src=\"cid:...\"> references."},
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
mailboxID := resolveComposeMailboxID(runtime)
content, _, err := resolveSignatureContent(runtime, "content", "content-file")
contentSource := "inline"
if runtime.Str("content-file") != "" {
contentSource = runtime.Str("content-file")
}
api := common.NewDryRunAPI().
Desc("Create a personal USER mail signature. The command validates content/images locally, then POSTs a signature wrapper.").
POST(mailboxPath(mailboxID, "settings", "signatures")).
Body(map[string]interface{}{
"signature": map[string]interface{}{
"name": strings.TrimSpace(runtime.Str("name")),
"content_source": contentSource,
"content_preview": contentPreview(content, 120, resolveLang(runtime)),
"signature_device": strings.ToUpper(runtime.Str("device")),
"images": "<parsed from --images-json>",
"signature_type": "USER",
},
})
if err != nil {
api.Set("validation_warning", err.Error())
}
return api
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if strings.TrimSpace(runtime.Str("name")) == "" {
return output.ErrValidation("--name is required")
}
if len([]rune(strings.TrimSpace(runtime.Str("name")))) > 100 {
return output.ErrValidation("--name must be at most 100 characters")
}
if runtime.Changed("content") && runtime.Str("content-file") != "" {
return output.ErrValidation("--content and --content-file are mutually exclusive")
}
if _, err := normalizeSignatureDevice(runtime.Str("device"), "--device"); err != nil {
return err
}
content, _, err := resolveSignatureContent(runtime, "content", "content-file")
if err != nil {
return err
}
images, _, err := parseSignatureImagesJSON(runtime.Str("images-json"), "images-json")
if err != nil {
return err
}
return validateSignatureImageRefs(content, images)
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
mailboxID := resolveComposeMailboxID(runtime)
device, err := normalizeSignatureDevice(runtime.Str("device"), "--device")
if err != nil {
return err
}
content, _, err := resolveSignatureContent(runtime, "content", "content-file")
if err != nil {
return err
}
images, _, err := parseSignatureImagesJSON(runtime.Str("images-json"), "images-json")
if err != nil {
return err
}
if err := validateSignatureImageRefs(content, images); err != nil {
return err
}

created, err := signature.Create(runtime, mailboxID, signature.Signature{
Name: strings.TrimSpace(runtime.Str("name")),
SignatureType: signature.SignatureTypeUser,
SignatureDevice: device,
Content: content,
Images: sanitizeSignatureImages(images),
})
if err != nil {
return err
}
out := signatureOutput(created, resolveLang(runtime))
delete(out, "changed_fields")
delete(out, "last_write_policy")
runtime.OutFormat(out, nil, func(w io.Writer) {
formatSignatureSummary(w, "created", created, resolveLang(runtime))
fmt.Fprintln(w, "Use this signature_id with mail +send / +reply / +forward --signature-id.")
})
Comment on lines +112 to +115
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move the follow-up hint to stderr.

Line 114 writes a usage hint through OutFormat, so non-result text is mixed into the command’s stdout stream. Keep the created signature data on stdout and send the hint to runtime.IO().ErrOut instead.

Proposed fix
 		runtime.OutFormat(out, nil, func(w io.Writer) {
 			formatSignatureSummary(w, "created", created, resolveLang(runtime))
-			fmt.Fprintln(w, "Use this signature_id with mail +send / +reply / +forward --signature-id.")
 		})
+		fmt.Fprintln(runtime.IO().ErrOut, "hint: use this signature_id with mail +send / +reply / +forward --signature-id.")
 		return nil
 	},

As per coding guidelines, stdout is data (JSON envelopes), stderr is everything else (progress, warnings, hints).

📝 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
runtime.OutFormat(out, nil, func(w io.Writer) {
formatSignatureSummary(w, "created", created, resolveLang(runtime))
fmt.Fprintln(w, "Use this signature_id with mail +send / +reply / +forward --signature-id.")
})
runtime.OutFormat(out, nil, func(w io.Writer) {
formatSignatureSummary(w, "created", created, resolveLang(runtime))
})
fmt.Fprintln(runtime.IO().ErrOut, "hint: use this signature_id with mail +send / +reply / +forward --signature-id.")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/mail/mail_signature_create.go` around lines 112 - 115, The
follow-up hint is being printed to stdout via runtime.OutFormat's writer; keep
the created signature output on stdout and move the usage hint to stderr by
removing the fmt.Fprintln(w, ...) call inside the runtime.OutFormat callback and
instead write the hint to runtime.IO().ErrOut (e.g.
fmt.Fprintln(runtime.IO().ErrOut, "Use this signature_id with mail +send /
+reply / +forward --signature-id.")) after or outside the OutFormat call; leave
formatSignatureSummary(...) and the OutFormat invocation unchanged so only the
hint is redirected to stderr.

return nil
},
}
86 changes: 86 additions & 0 deletions shortcuts/mail/mail_signature_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package mail

import (
"context"
"fmt"
"io"

"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/shortcuts/common"
"github.com/larksuite/cli/shortcuts/mail/signature"
)

var MailSignatureDelete = common.Shortcut{
Service: "mail",
Command: "+signature-delete",
Description: "Delete a personal USER mail signature after GET validation. Requires --yes for non-interactive safety.",
Risk: "high-risk-write",
Scopes: []string{"mail:user_mailbox.message:modify", "mail:user_mailbox:readonly"},
AuthTypes: []string{"user"},
HasFormat: true,
Flags: []common.Flag{
{Name: "mailbox", Desc: "Mailbox address that owns the signature (default: me)."},
{Name: "signature-id", Desc: "USER signature ID to delete.", Required: true},
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
mailboxID := resolveComposeMailboxID(runtime)
signatureID := runtime.Str("signature-id")
return common.NewDryRunAPI().
Desc("Delete a personal USER mail signature: GET current signatures, reject TENANT or missing targets, DELETE the signature, then best-effort GET to verify absence. Default signature application refs are cleared by the server.").
GET(mailboxPath(mailboxID, "settings", "signatures")).
DELETE(mailboxPath(mailboxID, "settings", "signatures", signatureID)).
GET(mailboxPath(mailboxID, "settings", "signatures"))
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if err := validateSignatureID(runtime.Str("signature-id")); err != nil {
return err
}
if runtime.Str("signature-id") == "" {
return output.ErrValidation("--signature-id is required")
}
if !runtime.Bool("dry-run") && !runtime.Bool("yes") {
return output.ErrValidation("--yes is required to delete a signature in non-interactive mode")
}
return nil
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
mailboxID := resolveComposeMailboxID(runtime)
signatureID := runtime.Str("signature-id")
current, err := findSignature(runtime, mailboxID, signatureID)
if err != nil {
return err
}
if current.SignatureType != signature.SignatureTypeUser {
return output.ErrValidation("only USER signatures can be deleted")
}
if err := signature.Delete(runtime, mailboxID, signatureID); err != nil {
return err
}
verifyStatus := "unknown"
if resp, err := signature.ListAll(runtime, mailboxID); err == nil {
verifyStatus = "absent"
for _, sig := range resp.Signatures {
if sig.ID == signatureID {
verifyStatus = "unknown"
break
}
}
}
out := map[string]interface{}{
"deleted": true,
"deleted_signature_id": signatureID,
"name": current.Name,
"signature_device": current.SignatureDevice,
"verify_status": verifyStatus,
}
runtime.OutFormat(out, nil, func(w io.Writer) {
fmt.Fprintln(w, "Signature deleted.")
fmt.Fprintf(w, "deleted_signature_id: %s\n", signatureID)
fmt.Fprintf(w, "verify_status: %s\n", verifyStatus)
})
return nil
},
}
Loading
Loading