mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
5e6a3eb857
* feat(mail): return typed error envelopes across the mail domain Replace every produced error path in shortcuts/mail with typed errs.* envelopes, so consumers get stable category, subtype, param/params, hint, retryable, and log_id metadata for classification and recovery instead of free-form message text. - Locally constructed mail errors move from output.Err* / output.Errorf / final fmt.Errorf / common legacy helpers to errs.* builders, with structured params on multi-flag validation and failed-precondition states kept non-retryable. - API-call failures move from runtime.CallAPI / DoAPIJSON legacy boundaries to runtime.CallAPITyped or runtime.ClassifyAPIResponse, and mail-specific enrichers read errs.ProblemOf so typed code, subtype, hint, and log_id metadata are preserved. - Batch draft-send partial failures now use runtime.OutPartialFailure so successful and failed draft sends stay in stdout while the command exits through a typed multi-status signal. - Add mail-domain typed helpers, mail API code metadata, and guard wiring to keep shortcuts/mail from reintroducing legacy envelopes or legacy API calls. - Keep genuine intermediate fmt.Errorf wraps in parser/builder layers annotated with nolint comments; command-facing paths wrap them into typed validation, API, network, or internal errors. * fix(mail): report aborted draft-send batches as a single failure result When an account-level failure interrupts a batch send after some drafts already went out, the command previously produced two machine-readable failure results: the partial-failure ledger on stdout and a second error envelope on stderr. Consumers could not tell which one to recover from. The batch ledger is now the only failure result for that case: it gains aborted and abort_error fields carrying the typed cause, so callers can see which drafts were sent, which failed, why the batch stopped, and how to recover — all from stdout. A --stop-on-error stop keeps these fields unset because stopping early there is the caller's own choice.
98 lines
4.2 KiB
Go
98 lines
4.2 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mail
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// MailDeclineReceipt is the `+decline-receipt` shortcut: dismiss the read-
|
|
// receipt request banner on an incoming message WITHOUT sending a receipt.
|
|
// Mirrors the Lark client's "不发送" button next to the read-receipt prompt —
|
|
// the client talks to the internal MessageModify RPC with RemoveLabelIds=
|
|
// ["-607"]; this shortcut calls the public OpenAPI user_mailbox.message.modify
|
|
// which accepts the symbolic "READ_RECEIPT_REQUEST" label name (the public
|
|
// endpoint performs the symbolic→numeric translation server-side). Either
|
|
// path lands on the same MessageModify codepath, closing the banner.
|
|
// Removes only the READ_RECEIPT_REQUEST system label; no outgoing mail is
|
|
// produced. Idempotent: running it on a message that no longer carries the
|
|
// label is a no-op, not an error.
|
|
var MailDeclineReceipt = common.Shortcut{
|
|
Service: "mail",
|
|
Command: "+decline-receipt",
|
|
Description: "Dismiss the read-receipt request banner on an incoming mail by clearing its READ_RECEIPT_REQUEST label, without sending a receipt. Use when the user wants to silence the prompt but refuse to confirm they have read it. Idempotent — safe to re-run.",
|
|
Risk: "write",
|
|
Scopes: []string{
|
|
"mail:user_mailbox.message:modify",
|
|
"mail:user_mailbox.message:readonly",
|
|
"mail:user_mailbox:readonly",
|
|
// fetchFullMessage(..., false) uses format=plain_text_full which the
|
|
// backend scope-checks against body:read even though we only inspect
|
|
// label_ids. Declared explicitly to keep Scopes truthful.
|
|
"mail:user_mailbox.message.body:read",
|
|
},
|
|
AuthTypes: []string{"user"},
|
|
Flags: []common.Flag{
|
|
{Name: "message-id", Desc: "Required. Message ID of the incoming mail that requested a read receipt.", Required: true},
|
|
{Name: "mailbox", Desc: "Mailbox email address that owns the message (default: me)."},
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
messageID := runtime.Str("message-id")
|
|
mailboxID := resolveComposeMailboxID(runtime)
|
|
return common.NewDryRunAPI().
|
|
Desc("Decline read receipt: fetch the original message → verify the READ_RECEIPT_REQUEST label is present → PUT user_mailbox.message.modify (the OpenAPI wrapper around the MessageModify RPC the Lark client's \"不发送\" button triggers) with remove_label_ids=[\"READ_RECEIPT_REQUEST\"]. No outgoing mail is produced; the banner is cleared locally. Idempotent when the label is already absent.").
|
|
GET(mailboxPath(mailboxID, "messages", messageID)).
|
|
Params(map[string]interface{}{"format": messageGetFormat(false)}).
|
|
PUT(mailboxPath(mailboxID, "messages", messageID, "modify")).
|
|
Body(map[string]interface{}{
|
|
"remove_label_ids": []string{readReceiptRequestLabel},
|
|
})
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
messageID := runtime.Str("message-id")
|
|
mailboxID := resolveComposeMailboxID(runtime)
|
|
|
|
msg, err := fetchFullMessage(runtime, mailboxID, messageID, false)
|
|
if err != nil {
|
|
return mailDecorateProblemMessage(err, "failed to fetch original message")
|
|
}
|
|
|
|
out := map[string]interface{}{
|
|
"message_id": messageID,
|
|
"decline_receipt_for_id": messageID,
|
|
}
|
|
|
|
if !hasReadReceiptRequestLabel(msg) {
|
|
out["declined"] = false
|
|
out["already_cleared"] = true
|
|
runtime.OutFormat(out, nil, func(w io.Writer) {
|
|
fmt.Fprintln(w, "Read-receipt request already cleared — nothing to do.")
|
|
fmt.Fprintf(w, "message_id: %s\n", messageID)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
if _, err := runtime.CallAPITyped("PUT",
|
|
mailboxPath(mailboxID, "messages", messageID, "modify"),
|
|
nil,
|
|
map[string]interface{}{
|
|
"remove_label_ids": []string{readReceiptRequestLabel},
|
|
},
|
|
); err != nil {
|
|
return mailDecorateProblemMessage(err, "failed to clear READ_RECEIPT_REQUEST label")
|
|
}
|
|
|
|
out["declined"] = true
|
|
runtime.OutFormat(out, nil, func(w io.Writer) {
|
|
fmt.Fprintln(w, "已关闭已读回执请求(未发送回执)/ Read-receipt request dismissed (no receipt sent).")
|
|
fmt.Fprintf(w, "message_id: %s\n", messageID)
|
|
})
|
|
return nil
|
|
},
|
|
}
|