Skip to content
Merged
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
8 changes: 6 additions & 2 deletions shortcuts/mail/draft/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ func UpdateWithRaw(runtime *common.RuntimeContext, mailboxID, draftID, rawEML st
return err
}

func Send(runtime *common.RuntimeContext, mailboxID, draftID string) (map[string]interface{}, error) {
return runtime.CallAPI("POST", mailboxPath(mailboxID, "drafts", draftID, "send"), nil, nil)
func Send(runtime *common.RuntimeContext, mailboxID, draftID, sendTime string) (map[string]interface{}, error) {
var bodyParams map[string]interface{}
if sendTime != "" {
bodyParams = map[string]interface{}{"send_time": sendTime}
}
return runtime.CallAPI("POST", mailboxPath(mailboxID, "drafts", draftID, "send"), nil, bodyParams)
}

func extractDraftID(data map[string]interface{}) string {
Expand Down
22 changes: 22 additions & 0 deletions shortcuts/mail/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"regexp"
"strconv"
"strings"
"time"

"github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/internal/auth"
Expand Down Expand Up @@ -1906,6 +1907,27 @@ func checkAttachmentSizeLimit(fio fileio.FileIO, filePaths []string, extraBytes
return nil
}

// validateSendTime checks that --send-time, if provided, requires --confirm-send,
// is a valid Unix timestamp in seconds, and is at least 5 minutes in the future.
func validateSendTime(runtime *common.RuntimeContext) error {
sendTime := runtime.Str("send-time")
if sendTime == "" {
return nil
}
if !runtime.Bool("confirm-send") {
return fmt.Errorf("--send-time requires --confirm-send to be set")
}
ts, err := strconv.ParseInt(sendTime, 10, 64)
if err != nil {
return fmt.Errorf("--send-time must be a valid Unix timestamp in seconds, got %q", sendTime)
}
minTime := time.Now().Unix() + 5*60
if ts < minTime {
return fmt.Errorf("--send-time must be at least 5 minutes in the future (minimum: %d, got: %d)", minTime, ts)
}
return nil
}

// validateConfirmSendScope checks that the user's token includes the
// mail:user_mailbox.message:send scope when --confirm-send is set.
// This scope is not declared in the shortcut's static Scopes (to keep the
Expand Down
7 changes: 6 additions & 1 deletion shortcuts/mail/mail_forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var MailForward = common.Shortcut{
{Name: "attach", Desc: "Attachment file path(s), comma-separated, appended after original attachments (relative path only)"},
{Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"<unique-id>\",\"file_path\":\"<relative-path>\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via <img src=\"cid:...\"> in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."},
{Name: "confirm-send", Type: "bool", Desc: "Send the forward immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."},
{Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."},
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
messageId := runtime.Str("message-id")
Expand All @@ -59,6 +60,9 @@ var MailForward = common.Shortcut{
if err := validateConfirmSendScope(runtime); err != nil {
return err
}
if err := validateSendTime(runtime); err != nil {
return err
}
if runtime.Bool("confirm-send") {
if err := validateComposeHasAtLeastOneRecipient(runtime.Str("to"), runtime.Str("cc"), runtime.Str("bcc")); err != nil {
return err
Expand All @@ -76,6 +80,7 @@ var MailForward = common.Shortcut{
attachFlag := runtime.Str("attach")
inlineFlag := runtime.Str("inline")
confirmSend := runtime.Bool("confirm-send")
sendTime := runtime.Str("send-time")

mailboxID := resolveComposeMailboxID(runtime)
sourceMsg, err := fetchComposeSourceMessage(runtime, mailboxID, messageId)
Expand Down Expand Up @@ -218,7 +223,7 @@ var MailForward = common.Shortcut{
hintSendDraft(runtime, mailboxID, draftID)
return nil
}
resData, err := draftpkg.Send(runtime, mailboxID, draftID)
resData, err := draftpkg.Send(runtime, mailboxID, draftID, sendTime)
if err != nil {
return fmt.Errorf("failed to send forward (draft %s created but not sent): %w", draftID, err)
}
Expand Down
7 changes: 6 additions & 1 deletion shortcuts/mail/mail_reply.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var MailReply = common.Shortcut{
{Name: "attach", Desc: "Attachment file path(s), comma-separated (relative path only)"},
{Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"<unique-id>\",\"file_path\":\"<relative-path>\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via <img src=\"cid:...\"> in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."},
{Name: "confirm-send", Type: "bool", Desc: "Send the reply immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."},
{Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."},
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
messageId := runtime.Str("message-id")
Expand All @@ -56,6 +57,9 @@ var MailReply = common.Shortcut{
if err := validateConfirmSendScope(runtime); err != nil {
return err
}
if err := validateSendTime(runtime); err != nil {
return err
}
return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "")
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
Expand All @@ -68,6 +72,7 @@ var MailReply = common.Shortcut{
attachFlag := runtime.Str("attach")
inlineFlag := runtime.Str("inline")
confirmSend := runtime.Bool("confirm-send")
sendTime := runtime.Str("send-time")

inlineSpecs, err := parseInlineSpecs(inlineFlag)
if err != nil {
Expand Down Expand Up @@ -181,7 +186,7 @@ var MailReply = common.Shortcut{
hintSendDraft(runtime, mailboxID, draftID)
return nil
}
resData, err := draftpkg.Send(runtime, mailboxID, draftID)
resData, err := draftpkg.Send(runtime, mailboxID, draftID, sendTime)
if err != nil {
return fmt.Errorf("failed to send reply (draft %s created but not sent): %w", draftID, err)
}
Expand Down
7 changes: 6 additions & 1 deletion shortcuts/mail/mail_reply_all.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var MailReplyAll = common.Shortcut{
{Name: "attach", Desc: "Attachment file path(s), comma-separated (relative path only)"},
{Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"<unique-id>\",\"file_path\":\"<relative-path>\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via <img src=\"cid:...\"> in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."},
{Name: "confirm-send", Type: "bool", Desc: "Send the reply immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."},
{Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."},
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
messageId := runtime.Str("message-id")
Expand All @@ -57,6 +58,9 @@ var MailReplyAll = common.Shortcut{
if err := validateConfirmSendScope(runtime); err != nil {
return err
}
if err := validateSendTime(runtime); err != nil {
return err
}
return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "")
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
Expand All @@ -70,6 +74,7 @@ var MailReplyAll = common.Shortcut{
attachFlag := runtime.Str("attach")
inlineFlag := runtime.Str("inline")
confirmSend := runtime.Bool("confirm-send")
sendTime := runtime.Str("send-time")

inlineSpecs, err := parseInlineSpecs(inlineFlag)
if err != nil {
Expand Down Expand Up @@ -195,7 +200,7 @@ var MailReplyAll = common.Shortcut{
hintSendDraft(runtime, mailboxID, draftID)
return nil
}
resData, err := draftpkg.Send(runtime, mailboxID, draftID)
resData, err := draftpkg.Send(runtime, mailboxID, draftID, sendTime)
if err != nil {
return fmt.Errorf("failed to send reply-all (draft %s created but not sent): %w", draftID, err)
}
Expand Down
7 changes: 6 additions & 1 deletion shortcuts/mail/mail_send.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var MailSend = common.Shortcut{
{Name: "attach", Desc: "Attachment file path(s), comma-separated (relative path only)"},
{Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"<unique-id>\",\"file_path\":\"<relative-path>\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via <img src=\"cid:...\"> in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."},
{Name: "confirm-send", Type: "bool", Desc: "Send the email immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."},
{Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."},
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
to := runtime.Str("to")
Expand Down Expand Up @@ -62,6 +63,9 @@ var MailSend = common.Shortcut{
if err := validateComposeHasAtLeastOneRecipient(runtime.Str("to"), runtime.Str("cc"), runtime.Str("bcc")); err != nil {
return err
}
if err := validateSendTime(runtime); err != nil {
return err
}
return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), runtime.Str("body"))
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
Expand All @@ -74,6 +78,7 @@ var MailSend = common.Shortcut{
attachFlag := runtime.Str("attach")
inlineFlag := runtime.Str("inline")
confirmSend := runtime.Bool("confirm-send")
sendTime := runtime.Str("send-time")

senderEmail := resolveComposeSenderEmail(runtime)

Expand Down Expand Up @@ -145,7 +150,7 @@ var MailSend = common.Shortcut{
hintSendDraft(runtime, mailboxID, draftID)
return nil
}
resData, err := draftpkg.Send(runtime, mailboxID, draftID)
resData, err := draftpkg.Send(runtime, mailboxID, draftID, sendTime)
if err != nil {
return fmt.Errorf("failed to send email (draft %s created but not sent): %w", draftID, err)
}
Expand Down
26 changes: 18 additions & 8 deletions skill-template/domains/mail.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
4. **回复** — `+reply` / `+reply-all`(默认存草稿,加 `--confirm-send` 则立即发送)
5. **转发** — `+forward`(默认存草稿,加 `--confirm-send` 则立即发送)
6. **新邮件** — `+send` 存草稿(默认),加 `--confirm-send` 发送
7. **确认投递** — 发送后用 `send_status` 查询投递状态,向用户报告结果
7. **确认投递** — 立即发送后用 `send_status` 查询投递状态,定时发送后在预定时间后再查询;取消定时发送用 `cancel_scheduled_send`
8. **编辑草稿** — `+draft-edit` 修改已有草稿。正文编辑通过 `--patch-file`:回复/转发草稿用 `set_reply_body` op 保留引用区,普通草稿用 `set_body` op

### CRITICAL — 首次使用任何命令前先查 `-h`
Expand All @@ -62,15 +62,17 @@ lark-cli mail user_mailbox.messages -h

### 命令选择:先判断邮件类型,再决定草稿还是发送

| 邮件类型 | 存草稿(不发送) | 直接发送 |
|----------|-----------------|---------|
| **新邮件** | `+send` 或 `+draft-create` | `+send --confirm-send` |
| **回复** | `+reply` 或 `+reply-all` | `+reply --confirm-send` 或 `+reply-all --confirm-send` |
| **转发** | `+forward` | `+forward --confirm-send` |
| 邮件类型 | 存草稿(不发送) | 直接发送 | 定时发送 |
|----------|-----------------|---------|----------|
| **新邮件** | `+send` 或 `+draft-create` | `+send --confirm-send` | `+send --confirm-send --send-time <unix_timestamp>` |
| **回复** | `+reply` 或 `+reply-all` | `+reply --confirm-send` 或 `+reply-all --confirm-send` | `+reply --confirm-send --send-time <unix_timestamp>` 或 `+reply-all --confirm-send --send-time <unix_timestamp>` |
| **转发** | `+forward` | `+forward --confirm-send` | `+forward --confirm-send --send-time <unix_timestamp>` |

- 有原邮件上下文 → 用 `+reply` / `+reply-all` / `+forward`(默认即草稿),**不要用 `+draft-create`**
- **发送前必须向用户确认收件人和内容,用户明确同意后才可加 `--confirm-send`**
- **发送后必须调用 `send_status` 确认投递状态**(详见下方说明)
- **立即发送后必须调用 `send_status` 确认投递状态**;定时发送(`--send-time`)在预定发送时间后再查询,取消定时发送用 `cancel_scheduled_send`(详见下方说明)

> **定时发送注意事项**:`--send-time` 必须与 `--confirm-send` 配合使用,不能单独使用。`send_time` 为 Unix 时间戳(秒),需至少为当前时间 + 5 分钟。

### 使用公共邮箱或别名(send_as)发信

Expand Down Expand Up @@ -109,14 +111,22 @@ lark-cli mail +send --mailbox me --from alias@example.com \

### 发送后确认投递状态

邮件发送成功后(收到 `message_id`),**必须**调用 `send_status` API 查询投递状态并向用户报告:
**立即发送(无 `--send-time`)**:邮件发送成功后(收到 `message_id`),**必须**调用 `send_status` API 查询投递状态并向用户报告:

```bash
lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me","message_id":"<发送返回的 message_id>"}'
```

返回每个收件人的投递状态(`status`):1=正在投递, 2=投递失败重试, 3=退信, 4=投递成功, 5=待审批, 6=审批拒绝。向用户简要报告结果,如有异常状态(退信/审批拒绝)需重点提示。

**定时发送(指定了 `--send-time`)**:定时发送不会立即产生 `message_id`,`send_status` 在定时发送成功后会返回"待发送"状态,**不建议在定时发送后立即查询**。可在预定发送时间后再查询。如需取消定时发送:

```bash
lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":"<draft_id>"}'
```

**取消后邮件会变回草稿**,可继续编辑或在之后重新发送。

### 撤回邮件

发送成功后,若响应中包含 `recall_available: true`,说明该邮件支持撤回(24 小时内已投递的邮件)。
Expand Down
28 changes: 20 additions & 8 deletions skills/lark-mail/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ metadata:
4. **回复** — `+reply` / `+reply-all`(默认存草稿,加 `--confirm-send` 则立即发送)
5. **转发** — `+forward`(默认存草稿,加 `--confirm-send` 则立即发送)
6. **新邮件** — `+send` 存草稿(默认),加 `--confirm-send` 发送
7. **确认投递** — 发送后用 `send_status` 查询投递状态,向用户报告结果
7. **确认投递** — 立即发送后用 `send_status` 查询投递状态,定时发送后在预定时间后再查询;取消定时发送用 `cancel_scheduled_send`
8. **编辑草稿** — `+draft-edit` 修改已有草稿。正文编辑通过 `--patch-file`:回复/转发草稿用 `set_reply_body` op 保留引用区,普通草稿用 `set_body` op

### CRITICAL — 首次使用任何命令前先查 `-h`
Expand All @@ -76,15 +76,17 @@ lark-cli mail user_mailbox.messages -h

### 命令选择:先判断邮件类型,再决定草稿还是发送

| 邮件类型 | 存草稿(不发送) | 直接发送 |
|----------|-----------------|---------|
| **新邮件** | `+send` 或 `+draft-create` | `+send --confirm-send` |
| **回复** | `+reply` 或 `+reply-all` | `+reply --confirm-send` 或 `+reply-all --confirm-send` |
| **转发** | `+forward` | `+forward --confirm-send` |
| 邮件类型 | 存草稿(不发送) | 直接发送 | 定时发送 |
|----------|-----------------|---------|----------|
| **新邮件** | `+send` 或 `+draft-create` | `+send --confirm-send` | `+send --confirm-send --send-time <unix_timestamp>` |
| **回复** | `+reply` 或 `+reply-all` | `+reply --confirm-send` 或 `+reply-all --confirm-send` | `+reply --confirm-send --send-time <unix_timestamp>` 或 `+reply-all --confirm-send --send-time <unix_timestamp>` |
| **转发** | `+forward` | `+forward --confirm-send` | `+forward --confirm-send --send-time <unix_timestamp>` |

- 有原邮件上下文 → 用 `+reply` / `+reply-all` / `+forward`(默认即草稿),**不要用 `+draft-create`**
- **发送前必须向用户确认收件人和内容,用户明确同意后才可加 `--confirm-send`**
- **发送后必须调用 `send_status` 确认投递状态**(详见下方说明)
- **立即发送后必须调用 `send_status` 确认投递状态**;定时发送(`--send-time`)在预定发送时间后再查询,取消定时发送用 `cancel_scheduled_send`(详见下方说明)

> **定时发送注意事项**:`--send-time` 必须与 `--confirm-send` 配合使用,不能单独使用。`send_time` 为 Unix 时间戳(秒),需至少为当前时间 + 5 分钟。

### 使用公共邮箱或别名(send_as)发信

Expand Down Expand Up @@ -123,14 +125,22 @@ lark-cli mail +send --mailbox me --from alias@example.com \

### 发送后确认投递状态

邮件发送成功后(收到 `message_id`),**必须**调用 `send_status` API 查询投递状态并向用户报告:
**立即发送(无 `--send-time`)**:邮件发送成功后(收到 `message_id`),**必须**调用 `send_status` API 查询投递状态并向用户报告:

```bash
lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me","message_id":"<发送返回的 message_id>"}'
```

返回每个收件人的投递状态(`status`):1=正在投递, 2=投递失败重试, 3=退信, 4=投递成功, 5=待审批, 6=审批拒绝。向用户简要报告结果,如有异常状态(退信/审批拒绝)需重点提示。

**定时发送(指定了 `--send-time`)**:定时发送不会立即产生 `message_id`,`send_status` 在定时发送成功后会返回"待发送"状态,**不建议在定时发送后立即查询**。可在预定发送时间后再查询。如需取消定时发送:

```bash
lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":"<draft_id>"}'
```

**取消后邮件会变回草稿**,可继续编辑或在之后重新发送。

### 撤回邮件

发送成功后,若响应中包含 `recall_available: true`,说明该邮件支持撤回(24 小时内已投递的邮件)。
Expand Down Expand Up @@ -292,6 +302,7 @@ lark-cli mail <resource> <method> [flags] # 调用 API

### user_mailbox.drafts

- `cancel_scheduled_send` — 取消定时发送
- `create` — 创建草稿
- `delete` — 删除指定邮箱账户下的单份邮件草稿。注意:对于草稿状态的邮件,只能使用本接口删除,禁止使用 trash_message;被删除的草稿数据无法恢复,请谨慎使用。
- `get` — 获取草稿详情
Expand Down Expand Up @@ -376,6 +387,7 @@ lark-cli mail <resource> <method> [flags] # 调用 API
| `user_mailboxes.accessible_mailboxes` | `mail:user_mailbox:readonly` |
| `user_mailboxes.profile` | `mail:user_mailbox:readonly` |
| `user_mailboxes.search` | `mail:user_mailbox.message:readonly` |
| `user_mailbox.drafts.cancel_scheduled_send` | `mail:user_mailbox.message:send` |
| `user_mailbox.drafts.create` | `mail:user_mailbox.message:modify` |
| `user_mailbox.drafts.delete` | `mail:user_mailbox.message:modify` |
| `user_mailbox.drafts.get` | `mail:user_mailbox.message:readonly` |
Expand Down
Loading
Loading