first commit
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: dingtalk-mail
|
||||
description: 钉钉邮箱读写、搜索、回复与转发。Use when 用户说发邮件/查邮件/回邮件/转发邮件/未读邮件/邮件搜索/邮箱附件。一句话发邮件时先用 dingtalk-contact 解析并确认收件人,再由本 skill 发送;不做钉钉消息(走 dingtalk-chat)、紧急通知(走 dingtalk-misc)。命令前缀:dws mail。
|
||||
metadata:
|
||||
cli_version: ">=0.2.14"
|
||||
category: product
|
||||
requires:
|
||||
bins:
|
||||
- dws
|
||||
---
|
||||
|
||||
# 钉钉邮箱 Skill
|
||||
|
||||
## 前置条件 — 执行操作前必读
|
||||
|
||||
> **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dingtalk-shared`](../dingtalk-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。
|
||||
|
||||
> 命令参考:[mail.md](references/mail.md)。复杂搜索、附件、批量处理、草稿等多步邮件场景参考:[09-mail.md](references/09-mail.md)。
|
||||
|
||||
<!-- VISIBLE_SHORTCUTS_START -->
|
||||
## Shortcuts(无专用脚本/recipe 时优先)
|
||||
|
||||
以下 shortcut 同时进入公开 catalog 与 Runtime Schema。先按本 skill 的意图表、脚本和 recipe 路由:存在精确覆盖该场景的专用脚本/recipe 时按其执行;否则用户意图命中时,shortcut 优先于手写原子命令。命令已选中时直接执行;只在参数或安全语义不确定时读取 Agent leaf Schema(例如 `dws schema --cli-path "mail +<shortcut>" --compact --format json`),在当前 Cobra flags 不确定时读取 `dws mail <shortcut> --help`。只有参数映射、接口绑定或 provenance 审计才省略 `--compact`。仅当现有路由和 reference 都无法定位低频能力时,才用 `dws shortcut list --service mail --format json` 批量发现。
|
||||
|
||||
| Shortcut | 风险 | 适用场景 |
|
||||
|---|---|---|
|
||||
| `dws mail +find-mail-user` | read | 按关键词搜索邮箱联系人并投影列表(姓名/昵称/邮箱/工号等) |
|
||||
| `dws mail +folder-list` | read | 列出顶层文件夹或指定父文件夹下的子文件夹 |
|
||||
| `dws mail +message` | read | 读取一封邮件的完整正文与附件元数据 |
|
||||
| `dws mail +messages` | read | 按请求顺序读取多封邮件并逐封验证身份 |
|
||||
| `dws mail +search-mail` | read | 按 KQL 关键词搜索邮件并投影列表(主题/发件人/时间/messageId) |
|
||||
| `dws mail +thread` | read | 读取完整邮件会话并精确验证 conversationId |
|
||||
| `dws mail +triage` | read | 列出或筛选邮件摘要,自动解析邮箱与收件箱 |
|
||||
| `dws mail +user-search` | read | 按关键词或工号搜索邮箱用户(仅企业邮箱) |
|
||||
<!-- VISIBLE_SHORTCUTS_END -->
|
||||
|
||||
## 意图表
|
||||
|
||||
| 用户说 | 命令 |
|
||||
|--------|------|
|
||||
| "发邮件给 a@b.com" | `dws mail mailbox list --format json` → `dws mail message send --from <邮箱> --to a@b.com --subject "<标题>" --content "<正文>" --format json` |
|
||||
| "回复 / 全部回复 / 转发" | `dws mail message reply` / `reply-all` / `forward` |
|
||||
| "今天未读邮件" | `python scripts/mail_unread_summary.py` |
|
||||
| "带抄送发送" | `python scripts/mail_send_with_cc.py --to a@b.com --cc c@d.com --subject "<标题>" --body "<正文>"` |
|
||||
|
||||
## 标准 SOP(必遵流程)
|
||||
|
||||
> 命中以下意图**必须**按对应 SOP 顺序执行;**禁止**跳步、替换命令、编造 email/messageId。每条命令必须带 `--format json`。收件邮箱**必须**真实解析,**禁止**猜测。
|
||||
|
||||
### SOP-1 拿邮箱(get-mailbox)
|
||||
|
||||
**触发**:我的邮箱/发件需要邮箱/查邮件需要邮箱。
|
||||
|
||||
1. **执行(必须)**:`dws mail mailbox list --format json`,取自己的 `email`(默认选企业邮箱 `type:ORG`);查**他人**邮箱用 `dws mail user search --keyword "<姓名>" --format json`,**禁止**用 `mailbox list` 查他人。
|
||||
|
||||
**禁止**:把 `mailbox list` 当作他人邮箱查询、猜测邮箱地址。
|
||||
|
||||
### SOP-2 查 / 搜邮件(search-mail)
|
||||
|
||||
**触发**:查邮件/搜邮件/某主题邮件/某人发的邮件。
|
||||
|
||||
1. **拿邮箱(必须)**:先按 SOP-1 取 `email`;用户已明确提供可跳过。
|
||||
2. **执行(必须)**:浏览文件夹 `dws mail message list --email <邮箱> --limit <n> --format json`;KQL 搜索 `dws mail message search --email <邮箱> --query "<KQL>" --limit 20 --format json`(KQL 如 `subject:周报`、`from:alice@x.com`、`folderId:2`、`hasAttachments:true`,**只通过 `--query` 传**)。
|
||||
3. **取正文(必须)**:`dws mail message get --email <邮箱> --id <messageId> --format json`;`messageId` 从列表/搜索结果取,**禁止**编造。
|
||||
|
||||
**禁止**:把 KQL 拆成多个 flag、跳过 `message list/search` 直接猜 messageId。
|
||||
|
||||
### SOP-3 发邮件(send-mail)
|
||||
|
||||
**触发**:发邮件/写邮件/群发。
|
||||
|
||||
1. **发件邮箱(必须)**:`dws mail mailbox list` 取自己邮箱。
|
||||
2. **收件邮箱(必须)**:地址直接用;姓名按 [mail.md](references/mail.md) "查找他人邮箱地址"流程(`mail user search` 等)获取,**禁止**猜测。
|
||||
3. **执行(必须)**:`dws mail message send --from <发件邮箱> --to <收件邮箱> --subject "<主题>" --content "<正文>" --format json`;按需 `--cc`/`--attachment`/`--inline-attachment`。
|
||||
4. **验证(必须)**:从发送返回取真实 `internetMessageId`,执行 `dws mail message verify --email <发件邮箱> --internet-message-id <internetMessageId> --format json` 查发送状态;不要把普通 `messageId` 传给 verify。
|
||||
|
||||
**禁止**:猜测收件邮箱、发送后不确认状态就答复"已发送"。
|
||||
|
||||
### SOP-4 回复 / 转发(reply-forward)
|
||||
|
||||
**触发**:回复邮件/回复全部/转发。
|
||||
|
||||
1. **拿邮箱 + 原邮件(必须)**:SOP-1 取邮箱;用户未给 `messageId` 时**必须**先走 SOP-2 定位原邮件 `messageId`。
|
||||
2. **执行(必须)**:回复 `dws mail message reply --from <邮箱> --id <messageId> --content "<正文>" --format json`;回复全部用 `reply-all`;转发 `dws mail message forward --from <邮箱> --to <收件邮箱> --id <messageId> --content "<附言>" --format json`。
|
||||
|
||||
**禁止**:未定位原邮件就回复/转发、编造 messageId。
|
||||
|
||||
## 高频硬约束
|
||||
|
||||
- 用户要"完整内容/看看这封邮件/正文"时,`message search` 命中后必须继续调用 `dws mail message get --email <邮箱> --id <messageId> --format json`;不要只列候选后停下。
|
||||
- 搜到多封邮件时,若用户给了明确主题、附件名、发件人或时间线索,先选最匹配的一封执行 `message get`;只有同等候选无法判断时才询问用户。
|
||||
- 附件链路固定三步:`message search` → `attachment list --email <邮箱> --id <messageId>` → `attachment download --email <邮箱> --message-id <messageId> --attachment-id <attachmentId> --name <文件名>`;不存在批量下载命令。
|
||||
- 写入类操作(发送、回复、转发、删除、批量移动)按安全策略确认;只读查看、搜索、附件列表、下载不需要确认。
|
||||
- 所有 `dws mail` 命令加 `--format json`,并复用同一封邮件的 `messageId`,不要重新搜索导致目标漂移。
|
||||
|
||||
## 跨产品协作
|
||||
|
||||
- 收件人是人名 → 先用 `dingtalk-contact` 取 `orgAuthEmail`
|
||||
- 钉钉内消息 → 切到 `dingtalk-chat`
|
||||
## 局部意图与短流程
|
||||
|
||||
- [局部意图消歧](references/intent-guide.md);[短流程](references/lite-recipes.md)。
|
||||
@@ -0,0 +1,45 @@
|
||||
# 邮件
|
||||
|
||||
> 邮件的 4 条 **lite** recipe:`mail-list-mailbox`、`mail-search`、`mail-send`、`mail-reply-forward`,见 [lite-recipes.md](./lite-recipes.md)。下列专用规则与消歧请在命中邮件场景且**超出**上述 lite 时阅读本文。
|
||||
> 产品命令见 [mail.md](./mail.md)。通用批量/并行见 [recipes/conventions.md](recipes/conventions.md)。
|
||||
|
||||
## 专用规则(#9 非 lite 步骤必守)
|
||||
|
||||
- **KQL 语法强制**:邮件搜索的查询条件**只能**通过 `--query` 参数以 KQL 语法传入(如 `subject:周报`),**禁止臆造** `--subject`、`--sender`、`--from-address` 等不存在的 flag。详见 [mail.md](./mail.md) 中 KQL 查询字段说明。
|
||||
- **邮箱地址前置**:大部分邮件命令需要 `--email` 或 `--from` 参数,执行前**必须**先通过 `mail mailbox list` 获取当前用户邮箱,禁止猜测邮箱地址。
|
||||
- **查找他人邮箱**:需要获取某人邮箱地址时,**不要用 `mailbox list`**(只返回自己的),必须走三路并发查询流程(见 [mail.md](./mail.md) 中「查找他人邮箱地址」章节)。
|
||||
- **附件下载三步走**:先 `message search` 搜索邮件获取 messageId,再 `attachment list` 获取附件 ID 和文件名,最后逐个 `attachment download` 下载。**不存在 `download_batch` / `download_all` 等批量下载命令,禁止编造**。
|
||||
- **危险操作确认**:`batch-delete` 执行前必须向用户确认,同意后加 `--yes`。
|
||||
|
||||
## 与其他场景消歧
|
||||
|
||||
- **"给某人发邮件"**(只知姓名不知邮箱)→ 先走「查找他人邮箱地址」三路并发,再 `mail-send`。
|
||||
- **"找某人邮箱"**(终点是获取邮箱地址)→ 三路并发查询,不走 `mail-search`。
|
||||
- **"搜某人发的邮件"**(终点是邮件内容)→ `mail-search`,KQL 用 `from:xxx`。
|
||||
- **"催+邮件"** → `mail-send` 发催促邮件,不是 #1 消息。
|
||||
- **"邮件+待办"** → 先 `mail-search` 找邮件内容,再走 #2 创建待办。
|
||||
|
||||
## Recipe 速查(本表步骤,非 SKILL lite)
|
||||
|
||||
| Recipe | 步骤 |
|
||||
|--------|------|
|
||||
| `mail-get` | `mail message get --email <邮箱> --id <messageId>` → 查看邮件完整内容(含正文) |
|
||||
| `mail-folder-list` | `mail folder list --email <邮箱>` → 列举文件夹;`--folder <id>` 查子文件夹 |
|
||||
| `mail-tag-list` | `mail tag list --email <邮箱>` → 列举邮件标签 |
|
||||
| `mail-thread-get` | `mail thread get --email <邮箱> --id <conversationId>` → 获取会话(邮件线程)详情 |
|
||||
| `mail-attachment-list` | `mail attachment list --email <邮箱> --id <messageId>` → 列举指定邮件的附件 |
|
||||
| `mail-attachment-download` | 1. `mail attachment list --email <邮箱> --id <messageId>` → 取附件 `id` 和 `name`<br>2. `mail attachment download --email <邮箱> --message-id <messageId> --attachment-id <attachmentId> --name <文件名>` |
|
||||
| `mail-batch-move` | `mail message batch-move --email <邮箱> --ids <id1,id2,...> --folder <folderId>`(常用 folderId: 2=收件箱, 6=已删除) |
|
||||
| `mail-batch-delete` | `mail message batch-delete --email <邮箱> --ids <id1,id2,...> --yes`(**危险操作,须先确认**) |
|
||||
| `mail-draft-create` | `mail draft create --from <邮箱> --subject "<标题>"` → 取 `messageId`(可选 `--to`、`--content`、`--cc`) |
|
||||
| `mail-draft-update` | `mail draft update --from <邮箱> --id <draftId> --subject "<新标题>"`(可选 `--content`、`--to`、`--cc`) |
|
||||
| `mail-draft-send` | `mail draft send --from <邮箱> --id <draftId>` |
|
||||
|
||||
## Full / 多步组合
|
||||
|
||||
| Recipe | 行动指南(固定路线) |
|
||||
|--------|---------------------|
|
||||
| search-and-download-attachment | 1. `mail mailbox list` → 取邮箱<br>2. `mail message search --email <邮箱> --query "<KQL>" --limit 20` → 取 `messageId` 列表<br>3. 对每封邮件执行 `mail attachment list --email <邮箱> --id <messageId>` → 列出附件取 `id` 和 `name`<br>4. 对每个附件逐个执行 `mail attachment download --email <邮箱> --message-id <messageId> --attachment-id <attachmentId> --name <文件名>`(**仅支持逐个下载,不存在批量下载命令**) |
|
||||
| search-reply-forward | 1. `mail mailbox list` → 取邮箱<br>2. `mail message search --email <邮箱> --query "<KQL>" --limit 10` → 取 `messageId`<br>3. 展示搜索结果供用户选择<br>4. 按用户指示执行 reply / reply-all / forward(参见 lite `mail-reply-forward`) |
|
||||
| batch-mail-cleanup | 1. `mail mailbox list` → 取邮箱<br>2. `mail message search --email <邮箱> --query "<KQL>" --limit 100` → 取多个 `messageId`<br>3. 展示列表供用户确认<br>4. `mail message batch-move --email <邮箱> --ids <id1,id2,...> --folder 6 ` 移到已删除;或 `batch-delete` 永久删除 |
|
||||
| send-to-person-by-name | 1. `mail mailbox list` → 取发件邮箱<br>2. 走「查找他人邮箱地址」三路并发查询获取收件人邮箱(见 [mail.md](./mail.md))<br>3. `mail message send --from <发件邮箱> --to <收件邮箱> --subject "<标题>" --content "<内容>"` |
|
||||
@@ -0,0 +1,8 @@
|
||||
# mail 局部意图消歧
|
||||
|
||||
本文件从单 Skill `intent-guide.md` 拆分而来,仅保留与本产品相关的跨产品消歧规则。
|
||||
|
||||
| 用户说... | 真实意图 | 应该用 | 不要用 | 理由 |
|
||||
|---|---|---|---|---|
|
||||
| "搜一下智能化方案/最近 OKR 相关邮件/最近发版相关消息" | 搜企业知识内容 | `aisearch enterprise` | `doc search` / `mail search` / `chat message search` | 跨文档、消息、日程、听记、邮件等企业内容语义检索走 enterprise;具体 `queries/types/time-range` 抽槽见 `aisearch.md` |
|
||||
| "我发给某人的消息/邮件/文档/今天我干了什么" | 搜行为记录 | `aisearch behavior` | `chat` / `mail` / `doc` / `report` | 关注“谁对什么做过什么”,走 behavior;具体 `behavior-type/direction/chat-scope` 抽槽见 `aisearch.md` |
|
||||
@@ -0,0 +1,45 @@
|
||||
# mail Lite Recipe
|
||||
|
||||
本文件从单 Skill `lite-recipes.md` 拆分而来,仅保留与本产品相关的轻量流程。
|
||||
|
||||
## #9 邮件
|
||||
|
||||
### mail-list-mailbox
|
||||
|
||||
查询当前用户自己的可用邮箱地址列表。**仅返回自己的邮箱**,不能查他人邮箱(查他人邮箱请走 [mail.md](./mail.md) 中「查找他人邮箱地址」三路并发查询流程)。
|
||||
|
||||
`mail mailbox list`
|
||||
|
||||
### mail-search
|
||||
|
||||
搜索邮件。**必须使用 KQL 语法通过 `--query` 传递查询条件**,禁止臆造 `--subject`、`--from` 等不存在的 flag。
|
||||
|
||||
1. 获取邮箱地址:`mail mailbox list` → 取用户邮箱地址。若用户已提供邮箱可跳过。
|
||||
2. 构造 KQL 查询:根据用户意图将搜索条件转为 KQL 表达式(详见 [mail.md](./mail.md) 中 KQL 查询字段说明)。
|
||||
- 按主题:`subject:周报`、`subject:"项目 进展"`(含空格须加双引号)
|
||||
- 按发件人:`from:alice@company.com` 或 `from:"张三"`
|
||||
- 按日期:`date>2025-06-01T00:00:00Z`(ISO8601 格式,必须含时间部分)
|
||||
- 按文件夹:`folderId:2`(2=收件箱, 1=已发送, 5=草稿, 6=已删除)
|
||||
- 按是否有附件:`hasAttachments:true`
|
||||
- 组合:`from:alice AND subject:周报 AND date>2025-06-01T00:00:00Z`
|
||||
3. 执行搜索:`mail message search --email <邮箱> --query "<KQL表达式>" --limit 20`
|
||||
4. 查看详情(按需):`mail message get --email <邮箱> --id <messageId>`
|
||||
|
||||
### mail-send
|
||||
|
||||
发送邮件。
|
||||
|
||||
1. 获取邮箱地址:`mail mailbox list` → 取用户邮箱作为 `--from`。
|
||||
2. 确定收件人:用户直接提供邮箱地址 → 直接使用;用户提供姓名或工号 → 走「查找他人邮箱地址」三路并发流程(见 [mail.md](./mail.md))。
|
||||
3. 发送:`mail message send --from <发件邮箱> --to <收件邮箱> --subject "<主题>" --content "<正文>"`(可选 `--cc`、`--attachment`、`--inline-attachment`)。
|
||||
|
||||
### mail-reply-forward
|
||||
|
||||
回复或转发邮件。
|
||||
|
||||
1. 获取邮箱地址:`mail mailbox list` → 取用户邮箱。
|
||||
2. 定位原始邮件:若用户未提供 messageId → 先用 `mail-search` 搜索定位。
|
||||
3. 执行:
|
||||
- 回复:`mail message reply --from <邮箱> --id <messageId>`(可选 `--to`、`--subject`、`--content`)
|
||||
- 回复全部:`mail message reply-all --from <邮箱> --id <messageId>`(可选 `--to`、`--subject`、`--content`)
|
||||
- 转发:`mail message forward --from <邮箱> --to <收件邮箱> --id <messageId>`(可选 `--subject`、`--content`)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
# 业务域通用规范
|
||||
|
||||
> 仅服务本 skill 已迁入的行动指南。安全门控、危险操作确认、`--format json` 等已在本 skill 的 `SKILL.md` 中定义,此处不重复。
|
||||
|
||||
## 批量查询规范
|
||||
|
||||
| # | 规范 |
|
||||
|---|------|
|
||||
| 1 | **并行查详情**:拿到多个 ID 后,用 `&` 合并到同一条 Shell 命令并行执行 + `wait`,**严禁逐条串行** |
|
||||
| 2 | **翻页**:分页接口须拉全直至无更多 |
|
||||
| 3 | **优先批量 API**:有批量接口则用批量;无则按 #1 并行 |
|
||||
| 4 | **群消息**:必须先 `chat search --query` 得 `openConversationId`,再 `chat message list --group <openConversationId> --time "<yyyy-MM-dd HH:mm:ss>" --direction older`;多群同条命令并行 |
|
||||
| 5 | **列表少轮次**:带条件搜索/列表 → 一次采全详情;**禁止**无新参数时重复同一 `list` / `search` |
|
||||
|
||||
## 多源并行采集(公共模式)
|
||||
|
||||
> recipe 引用方式:`按「多源并行采集」执行(关键词=<X>,时间=<Y>至<Z>)`。
|
||||
|
||||
- 同条 Shell:`&` 并行 + `wait`;分页须采全。
|
||||
- 只保留与主题相关的数据,无关丢弃。
|
||||
- 有批量详情接口优先;否则并行拉详情(见上表 #1)。
|
||||
- 具体采哪些产品列表由对应 **行动指南 recipe** 与当前产品参考决定;不要引入本文档未覆盖的产品路线。
|
||||
|
||||
## 字段术语与 ID 传递
|
||||
|
||||
> list 返回 JSON 后,必须提取下表字段传给后续命令。**禁止用其他字段替代。**
|
||||
|
||||
| 字段 | 来源 | 传递给 |
|
||||
|------|------|--------|
|
||||
| `taskUuid` | `minutes list` | `minutes get summary/info/batch --id(s)` |
|
||||
| `userId` | `aisearch person` / `contact user search` / `contact dept list-members` | `contact user get --ids`、`todo --executors`、`calendar --users` |
|
||||
| `deptId` | `contact dept search` | `contact dept list-members --ids <deptId1,deptId2...>`;多子部门时对每个子部门分别 `dept search` 取 id |
|
||||
| `nodeId` | `drive search` / `wiki node search` | `doc read/update --node`、`drive copy/move/rename/delete --node` |
|
||||
| `nodeId` | `wiki node list` 中的 folder 类型节点 / `wiki node create --type folder` | `wiki node list --folder`、`wiki node create --folder`、`drive upload --folder`、`drive copy/move --folder` |
|
||||
| `eventId` | `calendar event list` | `calendar event get/update --id` |
|
||||
| `processInstanceId` | `oa approval list-*` | `oa approval detail/approve --instance-id` |
|
||||
| `openConversationId` | `chat search` | `chat message list/send --group` |
|
||||
| `todoTaskId` | `todo task list` | `todo task update/done --task-id` |
|
||||
| `reportId` | `report inbox list` / `report outbox list` | `report entry get/stats --report-id` |
|
||||
| `baseId` / `tableId` | `aitable base search` | `aitable record query --base-id --table-id` |
|
||||
| `dentryUuid` | `drive list` / `drive mkdir` | `drive info/download/copy/move/rename/delete --node`、`drive list/mkdir/upload/copy/move --folder` |
|
||||
| `dentryId` | `drive info` 的数字字段 | 仅用于 `chat message send --dentry-id` |
|
||||
|
||||
**ID 边界硬约束**:`dentryId` 通常是纯数字,只表示聊天文件消息需要的钉盘条目数字 ID;它不是父目录 ID。遇到 `drive --node/--folder`、`doc --node`、`wiki node --folder` 时,只能使用 `dentryUuid` / `nodeId` / 文档 URL。若当前上下文只有数字型 `dentryId`,必须先重新 `drive list` / `drive search` / `wiki node list` 获取正确 ID,不能把该数字直接代入后续命令。
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
发送带抄送的邮件(自动获取发件地址、校验参数)
|
||||
|
||||
用法:
|
||||
python mail_send_with_cc.py \
|
||||
--to colleague@company.com \
|
||||
--cc boss@company.com,team@company.com \
|
||||
--subject "周报" \
|
||||
--body "本周完成任务A和任务B"
|
||||
|
||||
python mail_send_with_cc.py --dry-run \
|
||||
--to a@b.com --subject "test" --body "hello"
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import re
|
||||
import argparse
|
||||
from typing import List, Any, Optional
|
||||
|
||||
EMAIL_PATTERN = re.compile(
|
||||
r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$'
|
||||
)
|
||||
|
||||
|
||||
def run_dws(
|
||||
args: List[str], dry_run: bool = False,
|
||||
) -> Optional[Any]:
|
||||
cmd = ['dws'] + args
|
||||
if dry_run:
|
||||
print(f"[dry-run] {' '.join(cmd)}")
|
||||
return {'dry_run': True}
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=60
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"错误:{result.stderr.strip()}", file=sys.stderr)
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError,
|
||||
FileNotFoundError) as e:
|
||||
print(f"错误:{e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def validate_emails(emails_str: str) -> bool:
|
||||
for email in emails_str.split(','):
|
||||
email = email.strip()
|
||||
if not EMAIL_PATTERN.match(email):
|
||||
print(f"错误:无效邮箱地址 '{email}'")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_my_email(dry_run: bool = False) -> Optional[str]:
|
||||
data = run_dws([
|
||||
'mail', 'mailbox', 'list', '--format', 'json',
|
||||
], dry_run=dry_run)
|
||||
if dry_run:
|
||||
return '<MY_EMAIL>'
|
||||
if not data:
|
||||
return None
|
||||
if isinstance(data, list) and data:
|
||||
item = data[0]
|
||||
return (item.get('email') or item.get('address')
|
||||
if isinstance(item, dict) else str(item))
|
||||
if isinstance(data, dict):
|
||||
return data.get('email') or data.get('address')
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='发送带抄送的邮件'
|
||||
)
|
||||
parser.add_argument('--to', required=True, help='收件人')
|
||||
parser.add_argument('--cc', default='', help='抄送人')
|
||||
parser.add_argument('--subject', required=True, help='标题')
|
||||
parser.add_argument('--body', required=True, help='正文')
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
if not validate_emails(args.to):
|
||||
sys.exit(1)
|
||||
if args.cc and not validate_emails(args.cc):
|
||||
sys.exit(1)
|
||||
|
||||
print('📬 获取发件邮箱...')
|
||||
from_email = get_my_email(dry_run=args.dry_run)
|
||||
if not from_email and not args.dry_run:
|
||||
print('错误:无法获取发件邮箱')
|
||||
sys.exit(1)
|
||||
|
||||
cmd_args = [
|
||||
'mail', 'message', 'send',
|
||||
'--from', from_email or '<MY_EMAIL>',
|
||||
'--to', args.to,
|
||||
'--subject', args.subject,
|
||||
'--body', args.body,
|
||||
'--format', 'json',
|
||||
]
|
||||
if args.cc:
|
||||
cmd_args.extend(['--cc', args.cc])
|
||||
|
||||
print('📤 发送邮件...')
|
||||
result = run_dws(cmd_args, dry_run=args.dry_run)
|
||||
if result:
|
||||
print(f" ✓ 邮件已发送")
|
||||
print(f" 收件人: {args.to}")
|
||||
if args.cc:
|
||||
print(f" 抄送: {args.cc}")
|
||||
print(f" 主题: {args.subject}")
|
||||
else:
|
||||
print(' ✗ 发送失败')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
查询今天未读邮件并汇总(自动获取邮箱地址)
|
||||
|
||||
用法:
|
||||
python mail_unread_summary.py
|
||||
python mail_unread_summary.py --size 30
|
||||
python mail_unread_summary.py --dry-run
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import List, Any, Optional
|
||||
|
||||
TZ = timezone(timedelta(hours=8))
|
||||
|
||||
|
||||
def run_dws(
|
||||
args: List[str], dry_run: bool = False,
|
||||
) -> Optional[Any]:
|
||||
cmd = ['dws'] + args
|
||||
if dry_run:
|
||||
print(f"[dry-run] {' '.join(cmd)}")
|
||||
return None
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=60
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"错误:{result.stderr.strip()}", file=sys.stderr)
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError,
|
||||
FileNotFoundError) as e:
|
||||
print(f"错误:{e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def get_my_email(dry_run: bool = False) -> Optional[str]:
|
||||
data = run_dws([
|
||||
'mail', 'mailbox', 'list', '--format', 'json',
|
||||
], dry_run=dry_run)
|
||||
if dry_run:
|
||||
return '<MY_EMAIL>'
|
||||
if not data:
|
||||
return None
|
||||
if isinstance(data, list) and data:
|
||||
item = data[0]
|
||||
return (item.get('email') or item.get('address')
|
||||
if isinstance(item, dict) else str(item))
|
||||
if isinstance(data, dict):
|
||||
return data.get('email') or data.get('address')
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='查询今天未读邮件'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--size', type=int, default=20, help='返回数量'
|
||||
)
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
print('📬 获取邮箱地址...')
|
||||
email = get_my_email(dry_run=args.dry_run)
|
||||
if not email and not args.dry_run:
|
||||
print('错误:无法获取邮箱地址')
|
||||
sys.exit(1)
|
||||
|
||||
today = datetime.now(TZ).strftime('%Y-%m-%dT00:00:00Z')
|
||||
kql = f'isRead:false AND date>{today}'
|
||||
|
||||
print(f'🔍 搜索未读邮件...\n')
|
||||
data = run_dws([
|
||||
'mail', 'message', 'search',
|
||||
'--email', email or '<MY_EMAIL>',
|
||||
'--query', kql,
|
||||
'--size', str(args.size),
|
||||
'--format', 'json',
|
||||
], dry_run=args.dry_run)
|
||||
|
||||
if args.dry_run:
|
||||
return
|
||||
if not data:
|
||||
print('未查到邮件')
|
||||
return
|
||||
|
||||
messages = (data if isinstance(data, list)
|
||||
else data.get('items', data.get('messages', [])))
|
||||
|
||||
print(f"📧 今日未读邮件")
|
||||
print('=' * 50)
|
||||
if not messages:
|
||||
print(' ✅ 收件箱清空,没有未读邮件!')
|
||||
return
|
||||
|
||||
for m in messages:
|
||||
subj = m.get('subject', '(无主题)')
|
||||
sender = m.get('from', {})
|
||||
sender_name = (sender.get('name') or sender.get('email', '未知')
|
||||
if isinstance(sender, dict) else str(sender))
|
||||
print(f" 📩 {subj}")
|
||||
print(f" 发件人: {sender_name}")
|
||||
|
||||
print(f"\n合计: {len(messages)} 封未读邮件")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user