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
6 changes: 6 additions & 0 deletions shortcuts/mail/draft/htmltext.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@
return strings.Join(out, "\n")
}

// PlainTextFromHTML exposes the draft package's conservative HTML-to-text
// conversion for compose helpers that need to keep a text/plain body.
func PlainTextFromHTML(raw string) string {
return plainTextFromHTML(raw)

Check warning on line 90 in shortcuts/mail/draft/htmltext.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/mail/draft/htmltext.go#L89-L90

Added lines #L89 - L90 were not covered by tests
}

func bufLastByte(buf *bytes.Buffer) byte {
if buf.Len() == 0 {
return 0
Expand Down
1 change: 1 addition & 0 deletions shortcuts/mail/mail_lint_writepath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ func TestMailSend_WritePathLintAutofixesFontInEML(t *testing.T) {
"--to", "alice@example.com",
"--subject", "Send",
"--body", `<font color="red">payload</font>`,
"--no-signature",
"--show-lint-details",
}, f, stdout)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions shortcuts/mail/mail_request_receipt_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func TestMailSend_RequestReceiptAddsHeader_Integration(t *testing.T) {
"--to", "bob@example.com",
"--subject", "hi",
"--body", "please confirm",
"--no-signature",
"--request-receipt",
"--confirm-send",
}, f, stdout); err != nil {
Expand All @@ -153,6 +154,7 @@ func TestMailSend_RequestReceiptNoSender_FailsValidation(t *testing.T) {
"--to", "bob@example.com",
"--subject", "hi",
"--body", "body",
"--no-signature",
"--request-receipt",
"--confirm-send",
}, f, stdout)
Expand Down
26 changes: 16 additions & 10 deletions shortcuts/mail/mail_send.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
{Name: "request-receipt", Type: "bool", Desc: "Request a read receipt (Message Disposition Notification, RFC 3798) addressed to the sender. Recipient mail clients may prompt the user, send automatically, or silently ignore — delivery of a receipt is not guaranteed."},
{Name: "template-id", Desc: "Optional. Apply a saved template by ID (decimal integer string) before composing. The template's subject/body/to/cc/bcc/attachments are merged with user-supplied flags (user flags win). Requires --as user."},
signatureFlag,
noSignatureFlag,
priorityFlag,
eventSummaryFlag, eventStartFlag, eventEndFlag, eventLocationFlag,
showLintDetailsFlag},
Expand All @@ -58,7 +59,15 @@
Desc("Fetch template to merge with compose flags (subject/body/to/cc/bcc/attachments).")
}
api = api.GET(mailboxPath(mailboxID, "profile")).
POST(mailboxPath(mailboxID, "drafts")).
Desc("Resolve sender email for From header and signature matching.")
if !runtime.Bool("no-signature") {
desc := "Resolve default send signature for the sender."
if strings.TrimSpace(runtime.Str("signature-id")) != "" {
desc = "Resolve explicit signature by ID."

Check warning on line 66 in shortcuts/mail/mail_send.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/mail/mail_send.go#L66

Added line #L66 was not covered by tests
}
api = api.GET(mailboxPath(mailboxID, "settings", "signatures")).Desc(desc)

@qiooo qiooo Jun 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 AI Review | [P2 正确性] DryRun 少展示签名插值会调用的 send_as 请求

mail +send --dry-run 在非 --no-signature 分支只展示了 /settings/signatures,但真实执行在选中显式签名或默认签名后还会进入 resolveSenderInfo,调用 GET /settings/send_as 来解析签名模板中的 sender 变量。AI/脚本用 dry-run 预判权限、接口计划或准备 mock 时,会少配这个接口,导致 dry-run 与实际执行不一致。

修复建议: 在这个签名分支中补充 GET /settings/send_as,描述为用于签名模板 sender 变量插值;至少对 --signature-id 场景无条件展示,对默认签名场景说明命中默认签名后会调用。

如有疑问或认为判断不准确,欢迎直接回复讨论。

}
api = api.POST(mailboxPath(mailboxID, "drafts")).
Body(map[string]interface{}{
"raw": "<base64url-EML>",
"_preview": map[string]interface{}{
Expand Down Expand Up @@ -98,7 +107,7 @@
if err := validateSendTime(runtime); err != nil {
return err
}
if err := validateSignatureWithPlainText(runtime.Bool("plain-text"), runtime.Str("signature-id")); err != nil {
if err := validateSignatureFlags(runtime.Str("signature-id"), runtime.Bool("no-signature")); err != nil {
return err
}
// Resolve the body content first (reading --body-file if set) so
Expand Down Expand Up @@ -195,7 +204,8 @@
}
}

sigResult, err := resolveSignature(ctx, runtime, mailboxID, signatureID, senderEmail)
signatureHTMLMode := !plainText && bodyIsHTML(body)
sigResult, err := resolveSignatureForSend(ctx, runtime, mailboxID, signatureID, senderEmail, runtime.Bool("no-signature"), signatureHTMLMode)
if err != nil {
return err
}
Expand Down Expand Up @@ -230,14 +240,10 @@
// `lint_applied[]` / `original_blocked[]` even on the plain-text path.
lintApplied, lintBlocked := emptyLintEnvelopeFields()
if plainText {
composedTextBody = body
composedTextBody = injectSignatureIntoPlainText(body, sigResult)

Check warning on line 243 in shortcuts/mail/mail_send.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/mail/mail_send.go#L243

Added line #L243 was not covered by tests
bld = bld.TextBody([]byte(composedTextBody))
} else if bodyIsHTML(body) || sigResult != nil {
// If signature is requested on plain-text body, auto-upgrade to HTML.
} else if bodyIsHTML(body) {
htmlBody := body
if !bodyIsHTML(body) {
htmlBody = buildBodyDiv(body, false)
}
resolved, refs, resolveErr := draftpkg.ResolveLocalImagePaths(htmlBody)
if resolveErr != nil {
return mailValidationError("failed to resolve local image paths: %v", resolveErr).WithCause(resolveErr)
Expand Down Expand Up @@ -275,7 +281,7 @@
return err
}
} else {
composedTextBody = body
composedTextBody = injectSignatureIntoPlainText(body, sigResult)
bld = bld.TextBody([]byte(composedTextBody))
}
// Embed template SMALL non-inline attachments via AddAttachment.
Expand Down
3 changes: 3 additions & 0 deletions shortcuts/mail/mail_send_confirm_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ func TestMailSendConfirmSendOutputsAutomationDisable(t *testing.T) {
"--to", "alice@example.com",
"--subject", "hello",
"--body", "world",
"--no-signature",
"--confirm-send",
}, f, stdout)
if err != nil {
Expand Down Expand Up @@ -203,6 +204,7 @@ func TestMailSendSaveDraftOutputsReference(t *testing.T) {
"--to", "alice@example.com",
"--subject", "hello",
"--body", "world",
"--no-signature",
}, f, stdout)
if err != nil {
t.Fatalf("save draft failed: %v", err)
Expand Down Expand Up @@ -246,6 +248,7 @@ func TestMailSend_WithCalendarEventEmbedded(t *testing.T) {
"--event-summary", "Team Sync",
"--event-start", "2026-05-10T10:00+08:00",
"--event-end", "2026-05-10T11:00+08:00",
"--no-signature",
}, f, stdout)
if err != nil {
t.Fatalf("mail send with calendar failed: %v", err)
Expand Down
246 changes: 246 additions & 0 deletions shortcuts/mail/mail_send_signature_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package mail

import (
"strings"
"testing"

"github.com/larksuite/cli/internal/httpmock"
)

func registerSendSignatureList(reg *httpmock.Registry, mailboxID string, signatures []map[string]interface{}, usages []map[string]interface{}) {
reg.Register(&httpmock.Stub{
Method: "GET",
URL: mailboxPath(mailboxID, "settings", "signatures"),
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"signatures": signatures,
"usages": usages,
},
},
})
}

func registerSendAsForSignature(reg *httpmock.Registry, mailboxID, email string) {
reg.Register(&httpmock.Stub{
Method: "GET",
URL: mailboxPath(mailboxID, "settings", "send_as"),
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"sendable_addresses": []map[string]interface{}{
{"email_address": email, "name": "Sender"},
},
},
},
})
}

func registerDraftCaptureStubsForMailbox(reg *httpmock.Registry, mailboxID string) *httpmock.Stub {
createStub := &httpmock.Stub{
Method: "POST",
URL: mailboxPath(mailboxID, "drafts"),
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{"draft_id": "draft_001"},
},
}
reg.Register(createStub)
return createStub
}

func TestMailSendAppendsDefaultSignatureToHTMLBody(t *testing.T) {
f, stdout, _, reg := mailShortcutTestFactoryWithSendScope(t)
mailboxID := "sig-html-box"
sender := "sender@example.com"
registerSendSignatureList(reg, mailboxID,
[]map[string]interface{}{
{
"id": "sig_default",
"name": "Default",
"signature_type": "USER",
"signature_device": "PC",
"content": `<div>Best,<br>Alice</div>`,
},
},
[]map[string]interface{}{
{"email_address": sender, "send_mail_signature_id": "sig_default"},
},
)
registerSendAsForSignature(reg, mailboxID, sender)
createStub := registerDraftCaptureStubsForMailbox(reg, mailboxID)

err := runMountedMailShortcut(t, MailSend, []string{
"+send",
"--mailbox", mailboxID,
"--from", sender,
"--to", "bob@example.com",
"--subject", "hello",
"--body", "<p>Hello</p>",
}, f, stdout)
if err != nil {
t.Fatalf("send failed: %v", err)
}

raw := decodeCapturedRawEML(t, createStub.CapturedBody)
if !strings.Contains(raw, "lark-mail-signature") {
t.Fatalf("HTML EML missing signature wrapper:\n%s", raw)
}
if !strings.Contains(raw, "Best") || !strings.Contains(raw, "Alice") {
t.Fatalf("HTML EML missing signature content:\n%s", raw)
}
}

func TestMailSendAppendsDefaultSignatureToPlainTextBody(t *testing.T) {
f, stdout, _, reg := mailShortcutTestFactoryWithSendScope(t)
mailboxID := "sig-text-box"
sender := "sender@example.com"
registerSendSignatureList(reg, mailboxID,
[]map[string]interface{}{
{
"id": "sig_text",
"name": "Text",
"signature_type": "USER",
"signature_device": "PC",
"content": `<div>Best,<br>Alice</div>`,
},
},
[]map[string]interface{}{
{"email_address": sender, "send_mail_signature_id": "sig_text"},
},
)
registerSendAsForSignature(reg, mailboxID, sender)
createStub := registerDraftCaptureStubsForMailbox(reg, mailboxID)

err := runMountedMailShortcut(t, MailSend, []string{
"+send",
"--mailbox", mailboxID,
"--from", sender,
"--to", "bob@example.com",
"--subject", "hello",
"--body", "Hello",
}, f, stdout)
if err != nil {
t.Fatalf("send failed: %v", err)
}

raw := decodeCapturedRawEML(t, createStub.CapturedBody)
if !strings.Contains(raw, "Content-Type: text/plain") {
t.Fatalf("plain body should remain text/plain:\n%s", raw)
}
if strings.Contains(raw, "lark-mail-signature") || strings.Contains(raw, "Content-Type: text/html") {
t.Fatalf("plain body should not contain HTML signature wrapper:\n%s", raw)
}
if !strings.Contains(raw, "Hello") || !strings.Contains(raw, "Best") || !strings.Contains(raw, "Alice") {
t.Fatalf("plain EML missing body or signature text:\n%s", raw)
}
}

func TestMailSendNoSignatureSkipsSignatureLookup(t *testing.T) {
f, stdout, _, reg := mailShortcutTestFactoryWithSendScope(t)
mailboxID := "no-sig-box"
createStub := registerDraftCaptureStubsForMailbox(reg, mailboxID)

err := runMountedMailShortcut(t, MailSend, []string{
"+send",
"--mailbox", mailboxID,
"--from", "sender@example.com",
"--to", "bob@example.com",
"--subject", "hello",
"--body", "<p>Hello</p>",
"--no-signature",
}, f, stdout)
if err != nil {
t.Fatalf("send failed: %v", err)
}

raw := decodeCapturedRawEML(t, createStub.CapturedBody)
if strings.Contains(raw, "lark-mail-signature") || strings.Contains(raw, "Best") {
t.Fatalf("--no-signature should keep EML signature-free:\n%s", raw)
}
}

func TestMailSendExplicitSignatureOverridesDefault(t *testing.T) {
f, stdout, _, reg := mailShortcutTestFactoryWithSendScope(t)
mailboxID := "sig-explicit-box"
sender := "sender@example.com"
registerSendSignatureList(reg, mailboxID,
[]map[string]interface{}{
{
"id": "sig_default",
"name": "Default",
"signature_type": "USER",
"signature_device": "PC",
"content": `<div>Default Signature</div>`,
},
{
"id": "sig_explicit",
"name": "Explicit",
"signature_type": "USER",
"signature_device": "PC",
"content": `<div>Explicit Signature</div>`,
},
},
[]map[string]interface{}{
{"email_address": sender, "send_mail_signature_id": "sig_default"},
},
)
registerSendAsForSignature(reg, mailboxID, sender)
createStub := registerDraftCaptureStubsForMailbox(reg, mailboxID)

err := runMountedMailShortcut(t, MailSend, []string{
"+send",
"--mailbox", mailboxID,
"--from", sender,
"--to", "bob@example.com",
"--subject", "hello",
"--body", "<p>Hello</p>",
"--signature-id", "sig_explicit",
}, f, stdout)
if err != nil {
t.Fatalf("send failed: %v", err)
}

raw := decodeCapturedRawEML(t, createStub.CapturedBody)
if !strings.Contains(raw, "Explicit Signature") {
t.Fatalf("explicit signature missing from EML:\n%s", raw)
}
if strings.Contains(raw, "Default Signature") {
t.Fatalf("default signature should not be used when --signature-id is set:\n%s", raw)
}
}

func TestMailSendDryRunShowsSignatureLookupUnlessDisabled(t *testing.T) {
f, stdout, _, _ := mailShortcutTestFactoryWithSendScope(t)

if err := runMountedMailShortcut(t, MailSend, []string{
"+send",
"--to", "bob@example.com",
"--subject", "hello",
"--body", "<p>Hello</p>",
"--dry-run",
}, f, stdout); err != nil {
t.Fatalf("dry-run failed: %v", err)
}
if !strings.Contains(stdout.String(), "/settings/signatures") {
t.Fatalf("default dry-run should include signatures lookup:\n%s", stdout.String())
}

stdout.Reset()
if err := runMountedMailShortcut(t, MailSend, []string{
"+send",
"--to", "bob@example.com",
"--subject", "hello",
"--body", "<p>Hello</p>",
"--no-signature",
"--dry-run",
}, f, stdout); err != nil {
t.Fatalf("dry-run with --no-signature failed: %v", err)
}
if strings.Contains(stdout.String(), "/settings/signatures") {
t.Fatalf("--no-signature dry-run should omit signatures lookup:\n%s", stdout.String())
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
2 changes: 2 additions & 0 deletions shortcuts/mail/mail_template_shortcut_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,7 @@ func TestFetchTemplateAttachmentURLs_FailedReasons(t *testing.T) {
"--subject", "s",
"--body", "<p>b</p>",
"--template-id", "33",
"--no-signature",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "download URL not returned") {
t.Errorf("expected download-URL-missing error (with failed_reasons warning), got %v", err)
Expand Down Expand Up @@ -1141,6 +1142,7 @@ func TestMailSend_TemplateIDAppliesInlineAndSmall(t *testing.T) {
"--subject", "override-subj",
"--body", "<p>user body</p>",
"--template-id", "42",
"--no-signature",
}, f, stdout)
if err != nil {
t.Fatalf("+send --template-id failed: %v", err)
Expand Down
Loading
Loading