mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 22:24:31 +08:00
* feat(mail): bot+mailbox=me validation and dynamic --as help tests Add validateBotMailboxNotMe helper to shortcuts/mail/helpers.go and wire it as a Validate callback into +message, +messages, +thread and +triage, so bot identity combined with the default --mailbox me is rejected early with a clear fixup hint instead of a late opaque API error. The --as help text was already dynamic via AddShortcutIdentityFlag; add TC-10/TC-11 tests in internal/cmdutil/identity_flag_test.go to pin that behaviour, and TC-1 through TC-9 in shortcuts/mail/mail_shortcut_validation_test.go to cover the new Validate callbacks. +watch is excluded: its AuthTypes is ["user"], so bot is never valid. sprint: S2 * test(cmdutil): add Hidden and DefValue assertions to identity flag tests * fix(mail): add bot+mailbox=me validation to +template-create and +template-update * fix(mail): add bot+mailbox=me validation to +template-update * fix(mail): gofmt mail_template_create.go * fix(mail): gofmt mail_template_update.go * fix(mail): skip bot+mailbox=me check for print-patch-template local path
140 lines
5.2 KiB
Go
140 lines
5.2 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mail
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// mailThreadOutput is the +thread JSON output: the thread identifier,
|
|
// the number of messages in it, and the messages themselves in
|
|
// chronological order.
|
|
type mailThreadOutput struct {
|
|
ThreadID string `json:"thread_id"`
|
|
MessageCount int `json:"message_count"`
|
|
Messages []map[string]interface{} `json:"messages"`
|
|
}
|
|
|
|
// sortThreadMessagesByInternalDate filters out messages without a message_id
|
|
// and orders the rest ascending by internal_date (parsed via
|
|
// parseInternalDateMillis). Used to give +thread output a stable
|
|
// chronological order regardless of API return order.
|
|
func sortThreadMessagesByInternalDate(outs []map[string]interface{}) []map[string]interface{} {
|
|
messages := make([]map[string]interface{}, 0, len(outs))
|
|
for _, o := range outs {
|
|
if strVal(o["message_id"]) != "" {
|
|
messages = append(messages, o)
|
|
}
|
|
}
|
|
|
|
sort.Slice(messages, func(i, j int) bool {
|
|
di, _ := strconv.ParseInt(strVal(messages[i]["internal_date"]), 10, 64)
|
|
dj, _ := strconv.ParseInt(strVal(messages[j]["internal_date"]), 10, 64)
|
|
return di < dj
|
|
})
|
|
return messages
|
|
}
|
|
|
|
// MailThread is the `+thread` shortcut: fetch a full mail conversation by
|
|
// thread ID, returning every message in chronological order.
|
|
var MailThread = common.Shortcut{
|
|
Service: "mail",
|
|
Command: "+thread",
|
|
Description: "Use when querying a full mail conversation/thread by thread ID. Returns all messages in chronological order, including replies and drafts, with body content and attachments metadata, including inline images.",
|
|
Risk: "read",
|
|
Scopes: []string{"mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
HasFormat: true,
|
|
Flags: []common.Flag{
|
|
{Name: "mailbox", Default: "me", Desc: "email address (default: me)"},
|
|
{Name: "thread-id", Desc: "Required. Email thread ID", Required: true},
|
|
{Name: "html", Type: "bool", Default: "true", Desc: "Whether to return HTML body (false returns plain text only to save bandwidth)"},
|
|
{Name: "include-spam-trash", Type: "bool", Desc: "Also return messages from SPAM and TRASH folders (excluded by default)"},
|
|
{Name: "print-output-schema", Type: "bool", Desc: "Print output field reference (run this first to learn field names before parsing output)"},
|
|
},
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
return validateBotMailboxNotMe(runtime)
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
mailboxID := resolveMailboxID(runtime)
|
|
threadID := runtime.Str("thread-id")
|
|
params := map[string]interface{}{"format": messageGetFormat(runtime.Bool("html"))}
|
|
if runtime.Bool("include-spam-trash") {
|
|
params["include_spam_trash"] = true
|
|
}
|
|
return common.NewDryRunAPI().
|
|
Desc("Fetch all emails in thread with full body content").
|
|
GET(mailboxPath(mailboxID, "threads", threadID)).
|
|
Params(params)
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
if runtime.Bool("print-output-schema") {
|
|
printMessageOutputSchema(runtime)
|
|
return nil
|
|
}
|
|
mailboxID := resolveMailboxID(runtime)
|
|
hintIdentityFirst(runtime, mailboxID)
|
|
threadID := runtime.Str("thread-id")
|
|
html := runtime.Bool("html")
|
|
|
|
// List thread messages with full body content in one call.
|
|
params := map[string]interface{}{"format": messageGetFormat(html)}
|
|
if runtime.Bool("include-spam-trash") {
|
|
params["include_spam_trash"] = true
|
|
}
|
|
listData, err := runtime.CallAPI("GET", mailboxPath(mailboxID, "threads", threadID), params, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get thread: %w", err)
|
|
}
|
|
// New API: data.thread.messages[]; fallback to old API: data.items[].message
|
|
var items []interface{}
|
|
if thread, ok := listData["thread"].(map[string]interface{}); ok {
|
|
items, _ = thread["messages"].([]interface{})
|
|
}
|
|
if len(items) == 0 {
|
|
items, _ = listData["items"].([]interface{})
|
|
}
|
|
if len(items) == 0 {
|
|
runtime.Out(mailThreadOutput{ThreadID: threadID, MessageCount: 0, Messages: []map[string]interface{}{}}, nil)
|
|
return nil
|
|
}
|
|
|
|
outs := make([]map[string]interface{}, 0, len(items))
|
|
for _, item := range items {
|
|
envelope, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
// Old API wraps each message inside a "message" sub-object; new API puts fields directly.
|
|
msg := envelope
|
|
if inner, ok := envelope["message"].(map[string]interface{}); ok {
|
|
msg = inner
|
|
}
|
|
outs = append(outs, buildMessageOutput(msg, html))
|
|
}
|
|
|
|
// Sort by internal_date ascending.
|
|
messages := sortThreadMessagesByInternalDate(outs)
|
|
|
|
runtime.Out(mailThreadOutput{ThreadID: threadID, MessageCount: len(messages), Messages: messages}, nil)
|
|
for _, item := range items {
|
|
envelope, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
msg := envelope
|
|
if inner, ok := envelope["message"].(map[string]interface{}); ok {
|
|
msg = inner
|
|
}
|
|
maybeHintReadReceiptRequest(runtime, mailboxID, strVal(msg["message_id"]), msg)
|
|
}
|
|
return nil
|
|
},
|
|
}
|