Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
245 changes: 245 additions & 0 deletions shortcuts/mail/mail_send_signature_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// 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())
}

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