Files
larksuite__cli/shortcuts/mail/mail_decline_receipt.go
evandance 5e6a3eb857 feat(mail): return typed error envelopes across the mail domain (#1250)
* 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.
2026-06-04 21:02:20 +08:00

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
},
}