fix(mail-operations): 按现行邮件服务路由修正技能文档(1.0.6) (#194)

## 背景 / Background

发票整理 Agent 在隔离开发实例里真机整理 QQ 邮箱时,按内置技能 `mail-operations` 的写法调用 IMAP
附件下载,拿到 400 `Missing required parameters`:技能写的请求体是 `{email, uid, folder,
partId}`,而 `POST /imap/attachment` 从 2026-03 创建起就只认 `{email, messageId,
attachmentId, folder?}`。顺着这一处,对照 desirecore origin/dev(`c1e2c5ac0`)的
`lib/mail-service/` 与 `MailOperations` 工具把整份技能逐条核对了一遍。

While a real-mailbox run of the invoice agent was downloading QQ Mail
attachments, the builtin `mail-operations` skill's IMAP attachment body
(`{email, uid, folder, partId}`) returned 400; the route has always
taken `{email, messageId, attachmentId, folder?}`. This PR re-audits the
whole skill against the current mail service routes.

## 改动 / Changes

共修正 27 处不一致:会直接导致调用失败 9 处、会误导能力判断 10 处、措辞 8 处。主要有:

- **调用入口**:改为 `MailOperations`(path 带 `/api`,capability
由工具注入);原文的固定端口、「无需认证」、`curl -k` 都不成立
- **附件**:body 字段更正;必须带 `save_to` 落盘(不带会被工具拒绝返回),回执给出路径、字节数、SHA-256
- **参数位置**:`messages/fetch`、`/sync`、IMAP 草稿发送的 `email`(以及 `/sync` 的
`provider`)放查询串;IMAP 草稿收件人是逗号分隔的 `to` / `cc`,更新草稿会换成新 uid
- **folder**:IMAP 单封操作要用 `GET /imap/folders` 返回的服务器路径,列表里的归一化值会 500
- **规则**:operator 是 `matches_regex`(未知 operator 恒为 false
且创建时不校验);`archive` / `move_to_folder` / `forward_to` / `star` 是空操作
- **搜索**:三个 provider 都支持,只查本地缓存、`q` 只匹配主题;Gmail 全文检索改用 `messages/fetch`
的 `query`
- **IMAP 仅收信账户**:`allowUnverifiedSmtp`、`smtp_verification_failed`、409
`smtp_unverified`、`verify-smtp`(失败也返回 200)
- **离线队列**:不会自动入队或处理,只改本地,`processed` 计数不代表执行
- 响应字段名(`toRecipients`、`bodyPreview` 等)、同步状态文件、`/imap/test` 返回形态等措辞修正
- 版本 1.0.6;新增 `market.required_client_version: 10.0.148` 与 sidecar
`compatibility.requiredClientVersion`——`MailOperations`(10.0.95)、`save_to`
与 Outlook/IMAP search(10.0.146)、`imap:<uid>` / fetch `offset` / folder
归一化(10.0.148)分版本上线,旧客户端按新文档操作会静默失败,门控让它们保留旧版

## 验证 / Verification

- 写补丁与审查由两个独立子代理完成:审查方对 9 处致命项全部、另外 13 处抽查逐条对照代码行,均成立;审查意见(最低客户端版本、IMAP
更新草稿换 uid、`/sync` 参数、Gmail 远程检索、`verify-smtp` 返回形态、离线队列计数、响应示例矛盾)已全部采纳
- 各项最低版本用 `git tag --contains` 对照发布 tag 复核
- `translate.py --check`:en-US human-locked 且 hash 新鲜(`source_hash` 按
`compute_source_hash` 重算)
- `validate-i18n`:0 error,123 warning(与 main 基线相同)
- `validate_catalog_metadata --require-complete`:0 error
- `git diff --check` 通过;新增行只有 `example.com` 占位地址,无真实邮箱、姓名、公司名或长数字编号

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
2026-09-29 05:07:39 -04:00
committed by GitHub
parent d2125e5b2a
commit 74184056c4
4 changed files with 223 additions and 139 deletions

View File

@@ -10,7 +10,7 @@ description: >-
Use when 用户提到 邮件、邮箱、收件箱、发邮件、回复邮件、查邮件、Gmail、 Use when 用户提到 邮件、邮箱、收件箱、发邮件、回复邮件、查邮件、Gmail、
Outlook、QQ邮箱、163邮箱、附件、标签、草稿、自动回复、邮件规则、 Outlook、QQ邮箱、163邮箱、附件、标签、草稿、自动回复、邮件规则、
转发、抄送、未读邮件、收信、发信、邮件同步、邮件搜索。 转发、抄送、未读邮件、收信、发信、邮件同步、邮件搜索。
version: 1.0.5 version: 1.0.6
type: procedural type: procedural
risk_level: medium risk_level: medium
status: enabled status: enabled
@@ -24,7 +24,7 @@ tags:
- smtp - smtp
metadata: metadata:
author: desirecore author: desirecore
updated_at: '2026-06-20' updated_at: '2026-09-29'
i18n: i18n:
default_locale: en-US default_locale: en-US
source_locale: zh-CN source_locale: zh-CN
@@ -45,7 +45,7 @@ metadata:
description: >- description: >-
Use this skill whenever the user wants to interact with email. This includes reading inbox, sending emails, replying, searching messages, managing labels and categories, downloading attachments, setting up auto-reply rules, or triggering agents to handle incoming emails. Supports Gmail, Outlook, and IMAP/SMTP (QQ Mail, 163, Yahoo, etc.) through DesireCore's local REST API. Use when the user mentions email, mailbox, inbox, sending email, replying, checking email, Gmail, Outlook, QQ Mail, 163 Mail, attachments, labels, drafts, auto-reply, email rules, forwarding, CC, unread email, receiving, sending, email sync, or email search. Use this skill whenever the user wants to interact with email. This includes reading inbox, sending emails, replying, searching messages, managing labels and categories, downloading attachments, setting up auto-reply rules, or triggering agents to handle incoming emails. Supports Gmail, Outlook, and IMAP/SMTP (QQ Mail, 163, Yahoo, etc.) through DesireCore's local REST API. Use when the user mentions email, mailbox, inbox, sending email, replying, checking email, Gmail, Outlook, QQ Mail, 163 Mail, attachments, labels, drafts, auto-reply, email rules, forwarding, CC, unread email, receiving, sending, email sync, or email search.
body: ./SKILL.md body: ./SKILL.md
source_hash: sha256:0ee04ebff49d92ea source_hash: sha256:02d90959ea1d7e29
translated_by: human translated_by: human
translated_at: '2026-05-03' translated_at: '2026-05-03'
market: market:
@@ -63,6 +63,7 @@ market:
name: DesireCore Official name: DesireCore Official
verified: true verified: true
channel: latest channel: latest
required_client_version: 10.0.148
--- ---
# mail-operations Skill # mail-operations Skill
@@ -75,7 +76,7 @@ Send and receive email, search, manage labels, and run automation rules via a lo
### Capability ### Capability
mail-operations is a **Procedural Skill** that operates email systems through DesireCore's local REST API. It supports three mailbox types—Gmail (OAuth2), Outlook (MSAL), and IMAP/SMTP (QQ, 163, Yahoo, etc.)—covering send/receive, search, label management, attachment download, draft management, and automation rules. mail-operations is a **Procedural Skill** that operates email systems through DesireCore's local REST API (called via the built-in `MailOperations` tool). It supports three mailbox types—Gmail (OAuth2), Outlook (MSAL), and IMAP/SMTP (QQ, 163, Yahoo, etc.)—covering send/receive, search, label management, attachment download, draft management, and automation rules.
### Use Cases ### Use Cases
@@ -105,11 +106,12 @@ Users can reach the email management interface via:
## API Basics ## API Basics
- **Base URL**: `https://127.0.0.1:62000/api` - **How to call**: use the built-in `MailOperations` tool. Its parameters are `path` (a relative path starting with `/api/`, with query parameters appended directly to it), `method` (default `GET`), `body` (a JSON object), and `save_to` (only for attachment downloads, see section 6). The tool locates the local Mail Service and attaches the access credential automatically; the Agent neither needs nor can supply a token.
- **Authentication**: none required (local service) - **Path form**: the endpoints below omit the `/api` prefix; add it when calling. For example, `/imap/messages?email=me%40example.com&limit=20` becomes `path: "/api/imap/messages?email=me%40example.com&limit=20"`. The only exception is the health check `/ping` (no `/api`).
- **Content-Type**: `application/json` - **Do not connect to the port directly**: do not `curl` `https://127.0.0.1:62000`. The port is allocated dynamically starting at 62000 (when several DesireCore instances run, 62000 may belong to another instance), and in strict capability mode direct requests without the credential always return 401.
- **SSL**: use `curl -k` to skip self-signed certificate verification - **Parameter location**: GET / DELETE parameters always go in the query string; for POST / PUT follow the tables below. The `email` of endpoints such as `messages/fetch`, mark as read/unread, Outlook single-message operations, and IMAP draft send must be in the query string; putting it in the body returns 400 `Missing email parameter`. For `/sync`, both `provider` and `email` must be in the query string: `provider` in the body returns 400 `Invalid provider`, and `email` alone in the body raises no parameter error but the sync does not take effect.
- **Response format**: success `{"code": 1, "msg": "Success", "result": ...}`, failure `{"code": 0, "msg": "error message"}` - **Content-Type**: `application/json` (set automatically by the tool when `body` is passed)
- **Response format**: success `{"code": 1, "msg": "Success", "result": ...}`, failure `{"code": 0, "msg": "error message"}`; some errors also carry a stable error code field `error` (e.g. `smtp_unverified`)
--- ---
@@ -117,14 +119,14 @@ Users can reach the email management interface via:
The following rules have the highest priority and must be obeyed on every operation. The following rules have the highest priority and must be obeyed on every operation.
### Rule 1: Operate Only Through the Local API ### Rule 1: Operate Only Through the MailOperations Tool
All email operations **must and may only** be done through `https://127.0.0.1:62000`. **Never** call an external email client or browser. All email operations **must and may only** be done through the built-in `MailOperations` tool, which calls the local Mail Service. **Never** call an external email client or browser, and do not connect to the local port with `curl` / `HttpRequest` (except for the large-attachment streaming-to-disk method explicitly given in a `MailOperations` error message).
If the API returns **401 (authorization expired)**: If a call returns **401**, distinguish the cause by `msg`:
1. Tell the user the account's authorization has expired 1. Gmail / Outlook returns `Not authorized` or `Authorization expired`: tell the user the account's authorization has expired and guide them to re-authorize on the DesireCore Emails page. To start it on their behalf, call Gmail `POST /api/gmail/auth/initiate?loginHint={email}` or Outlook `POST /api/outlook/auth/initiate` and give the returned `result.authUrl` to the user to open in a browser (the service does not open a browser by itself)
2. Prompt the user to re-authorize in DesireCore 2. IMAP returns `Account not configured`: the account is missing or its configuration is corrupted; this is not an expired authorization, and the user needs to add the account again (first `/imap/test`, then `/imap/accounts`)
3. Gmail: `POST /api/gmail/auth/initiate?loginHint={email}`, Outlook: `POST /api/outlook/auth/initiate` 3. The response carries `error: "unauthorized"` (missing or invalid Mail Service capability): the request did not go through `MailOperations`; retry with that tool
### Rule 2: Confirm the Account Before Operating ### Rule 2: Confirm the Account Before Operating
@@ -136,7 +138,7 @@ Before performing any operation, **you must first call `GET /api/accounts`** to
### Rule 3: Auto-Sync When the Query Is Empty ### Rule 3: Auto-Sync When the Query Is Empty
When a query returns an empty list or cannot find the specified data: When a query returns an empty list or cannot find the specified data:
1. Call `POST /{provider}/messages/fetch` to sync remote data 1. Call `POST /{provider}/messages/fetch?email={email}` to sync remote data (`email` goes in the query string)
2. **Automatically retry** the original query 2. **Automatically retry** the original query
3. Only inform the user if it is still empty 3. Only inform the user if it is still empty
@@ -152,18 +154,20 @@ After a write operation (send, reply, delete, mark, label change, etc.) succeeds
|------|-------|---------|------| |------|-------|---------|------|
| Authorization | OAuth2 | OAuth2 (MSAL) | password / app password | | Authorization | OAuth2 | OAuth2 (MSAL) | password / app password |
| Provider path | `/gmail/` | `/outlook/` | `/imap/` | | Provider path | `/gmail/` | `/outlook/` | `/imap/` |
| Message detail | path param `/{id}` | query param `?id={id}` | UID `/{uid}?folder=` | | Message detail | path param `/{id}` | query param `?id={id}` | path param `/{uid}` (the list's `imap:<uid>` or a bare numeric UID) + `?folder=` |
| Search | supported | not supported | not supported | | Search (local cache) | supported | supported | supported |
| Drafts | supported (incl. list/detail) | supported (create/send) | supported (create/send) | | Remote fetch paging | `pageToken` (no `offset`) | `skip` / `offset` | `offset` |
| Drafts | supported (incl. list/detail) | supported (create/update/delete/send) | supported (create/update/delete/send) |
| Attachment download | supported | supported | supported | | Attachment download | supported | supported | supported |
| Labels/categories | native labels | Categories | local labels only | | Labels/categories | native labels | Categories | local labels only |
| Sending prerequisite | authorized | authorized | SMTP verified (`smtpStatus: verified`) |
| Auto-rules | supported | supported | supported | | Auto-rules | supported | supported | supported |
--- ---
## Core Operations ## Core Operations
In the following, `{p}` denotes the provider (`gmail`, `outlook`, `imap`); `{email}` must be URL-encoded. In the following, `{p}` denotes the provider (`gmail`, `outlook`, `imap`); `{email}` must be URL-encoded (`@` → `%40`, `+` → `%2B`).
### 1. Account Management ### 1. Account Management
@@ -181,31 +185,41 @@ In the following, `{p}` denotes the provider (`gmail`, `outlook`, `imap`); `{ema
| Operation | Method | Endpoint | | Operation | Method | Endpoint |
|------|------|------| |------|------|------|
| Get preset mailbox configs | GET | `/imap/presets` — returns server configs for QQ/163/Yahoo, etc. | | Get preset mailbox configs | GET | `/imap/presets` — returns server configs for QQ/163/Yahoo, etc. |
| Test IMAP connection | POST | `/imap/test` — body: `{email, password, imap: {host, port, secure}, smtp: {host, port, secure}}` | | Test IMAP connection | POST | `/imap/test` — body: `{email, password, imap?: {host, port, secure}, smtp?: {host, port, secure}}`; `imap` / `smtp` may be omitted when the domain has a preset. **A failed connection still returns HTTP 200**; check `result.imap.success` and `result.smtp.success` |
| Add IMAP account | POST | `/imap/accounts` — same body as above plus `displayName` | | Add IMAP account | POST | `/imap/accounts` — same body as above, optionally plus `displayName` and `allowUnverifiedSmtp`; success returns 201 with `{email, smtpStatus, smtp}`. If the SMTP test fails it returns 400 by default (`error: smtp_verification_failed`); pass `allowUnverifiedSmtp: true` only when the user agrees to a receive-only account |
| Re-verify SMTP | POST | `/imap/accounts/verify-smtp` — body: `{email}`; a failed verification also returns 200, so check `result.smtp.success` and `result.smtpStatus`: on success `smtpStatus` returns to `verified`; on failure the user needs to update the authorization code and verify again |
### 2. Message List and Sync ### 2. Message List and Sync
| Operation | Method | Endpoint | Parameters | | Operation | Method | Endpoint | Parameters |
|------|------|------|------| |------|------|------|------|
| Query local cache | GET | `/{p}/messages` | `email, offset, limit, folder` | | Query local cache | GET | `/{p}/messages` | query string: `email, offset, limit, folder`; returns `{messages, total, offset, limit}` |
| Remote sync | POST | `/{p}/messages/fetch` | `email, limit, folder` | | Remote sync | POST | `/{p}/messages/fetch?email=` | `email` must be in the query string; body (or query string): `folder`, `limit` (1–1000, default 20); paging: IMAP `offset`, Outlook `skip` / `offset`, Gmail `pageToken` (Gmail returns 400 for `offset`); returns `messages` plus paging info `pagination` |
| Manually trigger sync | POST | `/sync` | `provider, email` | | Manually trigger sync | POST | `/sync?provider=&email=` | parameters in the query string; **gmail / outlook only** — IMAP returns 400, use `messages/fetch` instead |
**folder values**: Gmail/Outlook: `inbox, sent, drafts, trash, spam, archive`; IMAP: `INBOX, Sent, Drafts, Trash`, etc. **IMAP remote sync is slow**: every message is downloaded and parsed in full (about 0.65 messages/second measured), so keep each batch's `limit` at 100 or less and page through older mail with `offset` (how many of the newest messages to skip).
**folder values**:
- Local cache list and search: `inbox, sent, drafts, trash, spam, archive, other`, case-insensitive; server names (e.g. `INBOX`, `Sent Messages`) are accepted too. IMAP custom folders are all stored locally as `other`.
- Remote sync: generic names (`inbox`, `sent`, ...) are mapped automatically to the actual IMAP folder, the Outlook folder, or the Gmail label; for IMAP you may also pass a `path` returned by `GET /imap/folders`. Gmail has no label for `archive`, so it fetches without filtering.
- **IMAP single-message operations** (detail on a cache miss, mark as read/unread, delete, reply, attachment download) pass `folder` verbatim to the IMAP server, so it must be a real `path` returned by `GET /imap/folders` (e.g. `INBOX`, `Sent Messages`); it defaults to `INBOX` when omitted. The `folder` field of a list item is a normalized value (e.g. `sent`) and cannot be used as an IMAP folder name directly.
**Response format** (message list item): **Response format** (message list item):
```json ```json
{ {
"id": "messageID", "subject": "subject", "id": "messageID (imap:<uid> for IMAP)", "provider": "gmail",
"subject": "subject",
"from": {"name": "...", "address": "..."}, "from": {"name": "...", "address": "..."},
"to": [{"name": "...", "address": "..."}], "toRecipients": [{"name": "...", "address": "..."}],
"date": "ISO8601", "snippet": "snippet", "receivedDateTime": "ISO8601", "sentDateTime": "ISO8601",
"bodyPreview": "snippet",
"isRead": true, "hasAttachments": false, "isRead": true, "hasAttachments": false,
"labels": ["INBOX"], "folder": "inbox" "folder": "inbox", "labelIds": ["INBOX"]
} }
``` ```
`labelIds` appears only on Gmail messages and `categories` only on Outlook messages.
### 3. Single Message Operations ### 3. Single Message Operations
| Operation | Gmail | Outlook | IMAP | | Operation | Gmail | Outlook | IMAP |
@@ -215,9 +229,9 @@ In the following, `{p}` denotes the provider (`gmail`, `outlook`, `imap`); `{ema
| Mark as unread | POST `/{id}/unread?email=` | POST `/message/unread?id={id}&email=` | POST `/{uid}/unread?email=&folder=` | | Mark as unread | POST `/{id}/unread?email=` | POST `/message/unread?id={id}&email=` | POST `/{uid}/unread?email=&folder=` |
| Delete | DELETE `/{id}?email=` | DELETE `/message?id={id}&email=` | DELETE `/{uid}?email=&folder=` | | Delete | DELETE `/{id}?email=` | DELETE `/message?id={id}&email=` | DELETE `/{uid}?email=&folder=` |
> All paths are prefixed with `/api/{provider}/messages` (Gmail/IMAP) or `/api/outlook/` (Outlook's special routing). > All paths are prefixed with `/api/{provider}/messages` (Gmail/IMAP) or `/api/outlook/` (Outlook's special routing). For IMAP, `{uid}` may be the list's `id` (`imap:<uid>`) as is; see section 2 for `folder`, which may be omitted for INBOX.
**Extra fields in message detail**: `body: {content, contentType}`, `cc`, `attachments: [{id, filename, mimeType, size}]` **Extra fields in message detail**: `body: {content, contentType}`, `ccRecipients`, `attachments: [{id, filename, contentType, size}]`. For IMAP, an attachment's `id` is its index within the message as a string (`"0"`, `"1"`, ...).
### 4. Send and Reply ### 4. Send and Reply
@@ -235,26 +249,35 @@ In the following, `{p}` denotes the provider (`gmail`, `outlook`, `imap`); `{ema
} }
``` ```
Each `attachments` item is `{filename, content (base64), contentType}`. Gmail requires at least one non-empty recipient. When an IMAP account's `smtpStatus` is `unverified` (receive-only), send, reply, and draft send all return 409 (`error: smtp_unverified`); guide the user to re-verify SMTP first.
**Reply to an email**: **Reply to an email**:
| Provider | Endpoint | Body | | Provider | Endpoint | Body |
|----------|------|------| |----------|------|------|
| Gmail | POST `/gmail/reply` | `{email, messageId, body, contentType}` | | Gmail | POST `/gmail/reply` | `{email, messageId, body, contentType}` |
| Outlook | POST `/outlook/message/reply?id={id}&email=` | `{body, contentType}` | | Outlook | POST `/outlook/message/reply?id={id}&email=` | `{body, contentType}` |
| IMAP | POST `/imap/reply` | `{email, uid, folder, body, contentType}` | | IMAP | POST `/imap/reply` | `{email, uid, folder, body, contentType}` (`uid` may be `imap:<uid>`; `folder` defaults to `INBOX`) |
### 5. Search (Gmail only) ### 5. Search (All Three Providers)
`GET /api/gmail/search?email={email}&q={keyword}` `GET /api/{p}/search?email={email}&q={keyword}` (`{p}` is `gmail`, `outlook`, or `imap`)
Search reads only the **local cache** and never contacts the mail server; if nothing is found, sync remotely per Rule 3 and search again.
| Parameter | Description | | Parameter | Description |
|------|------| |------|------|
| `q` | keyword (searches subject, body, sender) | | `q` | keyword, **matches the subject only** (case-insensitive) |
| `from` | sender address | | `from` | sender address (substring match) |
| `dateFrom` / `dateTo` | date range YYYY-MM-DD | | `dateFrom` / `dateTo` | date range YYYY-MM-DD (local time zone, both days inclusive) |
| `hasAttachment` | true/false | | `hasAttachment` | `true` returns only messages with attachments (`false` does not filter) |
| `isUnread` | true/false | | `isUnread` | `true` returns only unread messages (`false` does not filter) |
| `offset` / `limit` | pagination | | `folder` | folder filter, same values as the local cache list |
| `offset` / `limit` | pagination, `limit` at most 200 |
Parameter names must match exactly; unknown parameters (e.g. `query`, `hasAttachments`) return 400. Returns `{messages, total, offset, limit}`.
To run a full-text search on the Gmail server by body or sender, use the `query` parameter of `POST /gmail/messages/fetch?email=` instead: it is passed straight to the Gmail API, supports Gmail search syntax (such as `from:` and `has:attachment`), and the results are also written to the local cache. Outlook and IMAP have no equivalent remote search parameter. If an older client returns 404 for `/outlook/search` or `/imap/search`, page through `GET /{p}/messages` and filter the results yourself.
### 6. Attachment Download ### 6. Attachment Download
@@ -262,9 +285,23 @@ In the following, `{p}` denotes the provider (`gmail`, `outlook`, `imap`); `{ema
|----------|------|------|------| |----------|------|------|------|
| Gmail | POST | `/gmail/messages/{messageId}/attachment` | `{email, attachmentId}` | | Gmail | POST | `/gmail/messages/{messageId}/attachment` | `{email, attachmentId}` |
| Outlook | POST | `/outlook/attachment` | `{email, messageId, attachmentId}` | | Outlook | POST | `/outlook/attachment` | `{email, messageId, attachmentId}` |
| IMAP | POST | `/imap/attachment` | `{email, uid, folder, partId}` | | IMAP | POST | `/imap/attachment` | `{email, messageId, attachmentId, folder?}` |
The response `result.data` is base64-encoded; decode it and save to a file. - `attachmentId` comes from `attachments[].id` in the message detail; call the detail endpoint first to get the attachment list.
- IMAP: use the list's `id` (e.g. `imap:12345`) as `messageId`; `attachmentId` is the attachment index as a string (the first attachment is `"0"`); `folder` is the real folder path on the server and may be omitted for INBOX.
- **Always pass `save_to` to `MailOperations` when downloading an attachment** (the target file path): the tool decodes the base64 in process and writes the file, returning only the absolute path, byte count, and SHA-256; without `save_to` the tool refuses to return the attachment response. The target path must be inside a writable working directory.
- The endpoint's raw response is `result: {data: <base64>, size}`; the tool refuses to parse responses larger than about 32 MB — in that case follow the streaming-to-disk method given in the tool's error message, or ask the user to export the attachment manually from the Emails page.
Example (IMAP):
```json
{
"path": "/api/imap/attachment",
"method": "POST",
"body": {"email": "me@example.com", "messageId": "imap:12345", "attachmentId": "0"},
"save_to": "./attachments/report.pdf"
}
```
> Gmail uses POST because the attachmentId may exceed URL length limits. > Gmail uses POST because the attachmentId may exceed URL length limits.
@@ -276,7 +313,7 @@ The response `result.data` is base64-encoded; decode it and save to a file.
|------|------|------| |------|------|------|
| List drafts | GET | `/gmail/drafts?email=&limit=` | | List drafts | GET | `/gmail/drafts?email=&limit=` |
| Get draft detail | GET | `/gmail/drafts/{draftId}?email=` | | Get draft detail | GET | `/gmail/drafts/{draftId}?email=` |
| Create draft | POST | `/gmail/drafts` — body: `{email, to, cc, subject, body, contentType}` | | Create draft | POST | `/gmail/drafts` — body: `{email, to, cc, subject, body, contentType}` (`to` / `cc` are comma-separated address strings) |
| Update draft | PUT | `/gmail/drafts/{draftId}` — same body as create | | Update draft | PUT | `/gmail/drafts/{draftId}` — same body as create |
| Delete draft | DELETE | `/gmail/drafts/{draftId}?email=` | | Delete draft | DELETE | `/gmail/drafts/{draftId}?email=` |
@@ -293,10 +330,10 @@ The response `result.data` is base64-encoded; decode it and save to a file.
| Operation | Method | Endpoint | | Operation | Method | Endpoint |
|------|------|------| |------|------|------|
| Create draft | POST | `/imap/drafts` — body: `{email, toRecipients, subject, body, contentType}` | | Create draft | POST | `/imap/drafts` — body: `{email, to, cc, subject, body, contentType}` (`to` / `cc` are comma-separated address strings, not a `toRecipients` array); returns `{uid, id}` |
| Update draft | PUT | `/imap/drafts/{uid}` — body: `{email, folder, toRecipients, subject, body, contentType}` | | Update draft | PUT | `/imap/drafts/{uid}` — same body as create; it deletes the old draft and creates a new one, returning a **new** `{uid, id}` — use the new uid to send or delete afterwards (the old uid gets 404 `Draft not found`). If the server does not support UIDPLUS the returned uid is 0; re-read the draft list before the next operation |
| Delete draft | DELETE | `/imap/drafts/{uid}?email=&folder=` | | Delete draft | DELETE | `/imap/drafts/{uid}?email=` |
| Send draft | POST | `/imap/drafts/{uid}/send` — body: `{email, folder}` | | Send draft | POST | `/imap/drafts/{uid}/send?email=` — `email` in the query string, no body needed |
### 8. Label Management (Unified Interface) ### 8. Label Management (Unified Interface)
@@ -311,7 +348,9 @@ The response `result.data` is base64-encoded; decode it and save to a file.
| Add label to email | POST | `/mails/{p}/{email}/labels?mailId=` — body: `{"labelId": "..."}` | | Add label to email | POST | `/mails/{p}/{email}/labels?mailId=` — body: `{"labelId": "..."}` |
| Bulk set labels | PUT | `/mails/{p}/{email}/labels?mailId=` — body: `{"labelIds": [...]}` | | Bulk set labels | PUT | `/mails/{p}/{email}/labels?mailId=` — body: `{"labelIds": [...]}` |
| Remove label from email | DELETE | `/mails/{p}/{email}/labels?mailId=&labelId=` | | Remove label from email | DELETE | `/mails/{p}/{email}/labels?mailId=&labelId=` |
| Get emails under a label | GET | `/labels/{labelId}/mails?provider=&email=&limit=&offset=` | | Get emails under a label | GET | `/labels/{labelId}/mails?provider=&email=&limit=&offset=` — returns `mailIds` (no message content) |
The unified label interface changes only DesireCore's local data and does not sync to the mail server. To change labels on the server, use the Gmail native labels below or the Outlook categories in section 9.
**Gmail native labels**: **Gmail native labels**:
- List labels: `GET /api/gmail/labels?email=` - List labels: `GET /api/gmail/labels?email=`
@@ -355,46 +394,47 @@ Outlook uses Categories instead of Labels.
"email": "xxx@gmail.com", "email": "xxx@gmail.com",
"enabled": true, "enabled": true,
"conditions": [ "conditions": [
{"field": "from|to|subject|body|has_attachment", "operator": "contains|equals|regex|...", "value": "..."} {"field": "from|to|subject|body|has_attachment", "operator": "contains|not_contains|equals|not_equals|starts_with|ends_with|matches_regex|is_true|is_false", "value": "..."}
], ],
"conditionLogic": "and", "conditionLogic": "and",
"actions": [ "actions": [
{"type": "add_label|remove_label|mark_as_read|mark_as_unread|archive|delete|auto_reply|agent_handle", "value": "..."} {"type": "add_label|remove_label|mark_as_read|mark_as_unread|delete|auto_reply|agent_handle", "value": "..."}
], ],
"priority": 1, "priority": 1,
"stopOnMatch": false "stopOnMatch": false
} }
``` ```
The regex operator is `matches_regex` (`regex` is treated as an unknown operator and never matches); `is_true` / `is_false` are only for `has_attachment`.
**Action types**: **Action types**:
| type | value | Description | | type | value | Description |
|------|-------|------| |------|-------|------|
| `add_label` | label ID | add a label | | `add_label` | label ID | add a local label |
| `remove_label` | label ID | remove a label | | `remove_label` | label ID | remove a local label |
| `mark_as_read` | omit | mark as read | | `mark_as_read` | omit | mark as read in the local cache only |
| `mark_as_unread` | omit | mark as unread | | `mark_as_unread` | omit | mark as unread in the local cache only |
| `archive` | omit | archive | | `delete` | omit | delete from the local cache only; the message on the server is untouched |
| `delete` | omit | delete |
| `auto_reply` | reply text | auto-reply with fixed content | | `auto_reply` | reply text | auto-reply with fixed content |
| `agent_handle` | Agent ID | trigger an Agent to handle the email | | `agent_handle` | Agent ID | trigger an Agent to handle the email |
> Rules are **executed automatically** when the polling engine detects a new email; no manual call is required. `auto_reply` and `agent_handle` support all three mailbox types. > Rules are **executed automatically** when the polling engine detects a new email; no manual call is required (for IMAP only new INBOX mail is covered). `auto_reply` and `agent_handle` support all three mailbox types; `agent_handle` binds the Agent Service connection that is current when the rule is created or updated. `archive`, `move_to_folder`, `forward_to`, and `star` can be saved but currently do nothing when executed; do not promise these effects to the user.
### 11. Authorization Management ### 11. Authorization Management
| Operation | Method | Endpoint | | Operation | Method | Endpoint |
|------|------|------| |------|------|------|
| Gmail OAuth | POST | `/gmail/auth/initiate?loginHint={email}` — opens browser for authorization | | Gmail OAuth | POST | `/gmail/auth/initiate?loginHint={email}` — returns `result.authUrl` for the user to open in a browser to authorize |
| Gmail status | GET | `/gmail/auth/status?email=` | | Gmail status | GET | `/gmail/auth/status?email=` |
| Outlook OAuth | POST | `/outlook/auth/initiate` — opens browser for authorization | | Outlook OAuth | POST | `/outlook/auth/initiate` — returns `result.authUrl` for the user to open in a browser to authorize |
| Outlook status | GET | `/outlook/auth/status?email=` | | Outlook status | GET | `/outlook/auth/status?email=` |
### 12. Folders ### 12. Folders
| Operation | Method | Endpoint | | Operation | Method | Endpoint |
|------|------|------| |------|------|------|
| IMAP folder list | GET | `/imap/folders?email=` | | IMAP folder list | GET | `/imap/folders?email=` — returns `[{name, path, specialUse, flags}]`; use `path` as the `folder` of IMAP single-message operations |
| Outlook folder list | GET | `/outlook/folders?email=` | | Outlook folder list | GET | `/outlook/folders?email=` |
> Gmail folders are fixed: inbox, sent, drafts, trash, spam, archive. > Gmail folders are fixed: inbox, sent, drafts, trash, spam, archive.
@@ -405,11 +445,11 @@ Outlook uses Categories instead of Labels.
The email system uses **local cache + periodic polling**: The email system uses **local cache + periodic polling**:
- **Write operations** (send, mark, delete, label): update local and remote simultaneously, no delay - **Write operations** (send, mark, delete, Gmail native labels, Outlook categories): the provider endpoints update local and remote simultaneously, no delay; the unified label interface (section 8) and the mark, delete, and label actions of auto-rules change local data only
- **Read operations** (query, search): return the local cache; may be delayed (default 30-second polling) - **Read operations** (query, search): return the local cache; may be delayed (default 30-second polling)
- **Remote changes** (the user operates from the official web UI): wait for the next polling cycle to sync - **Remote changes** (the user operates from the official web UI): Gmail / Outlook wait for the next polling cycle to sync; IMAP polling only pulls new INBOX mail, other folders require an explicit `messages/fetch`, and read or delete changes made on the server to already-cached messages are not written back locally
**Storage path**: `${DESIRECORE_ROOT}/mail/{provider}/{email}/` (index.json, messages/, sync.json) **Storage path**: `${DESIRECORE_ROOT}/mail/{provider}/{email}/` (meta.json, index.json, messages/)
--- ---
@@ -417,9 +457,10 @@ The email system uses **local cache + periodic polling**:
| Status code | Reason | Handling | | Status code | Reason | Handling |
|--------|------|------| |--------|------|------|
| 400 | parameter error | check the request parameters | | 400 | parameter error (including a parameter in the wrong place, e.g. a query-string parameter put in the body) | check parameter names and locations against `msg` |
| 401 | authorization expired | **handle per Rule 1**; do not try other channels | | 401 | Gmail/Outlook authorization expired, IMAP account not configured, or missing Mail Service capability | **handle per Rule 1**; do not try other channels |
| 404 | resource not found | sync first and retry (Rule 3) | | 404 | resource not found | sync first and retry (Rule 3) |
| 500 | internal error | tell the user to retry later | | 409 | IMAP account SMTP not verified (`error: smtp_unverified`) | guide the user to re-verify SMTP (`POST /imap/accounts/verify-smtp`) |
| 500 | internal error (common IMAP cause: `folder` is not a folder that exists on the server) | check `folder` first; if it still fails, tell the user to retry later |
**IMAP note**: domestic Chinese mailboxes (QQ, 163) require an "app password" instead of the login password. Use `/imap/test` to validate the configuration in advance. **IMAP note**: domestic Chinese mailboxes (QQ, 163) require an "app password" instead of the login password. Use `/imap/test` to validate the configuration in advance and check `result.imap.success` and `result.smtp.success` in the response.

View File

@@ -10,7 +10,7 @@
### 能力描述 ### 能力描述
mail-operations 是一个**流程型技能(Procedural Skill)**,通过 DesireCore 本地 REST API 操作邮件系统。支持 Gmail(OAuth2)、Outlook(MSAL)和 IMAP/SMTP(QQ、163、Yahoo 等)三种邮箱类型,涵盖收发邮件、搜索、标签管理、附件下载、草稿管理和自动规则。 mail-operations 是一个**流程型技能(Procedural Skill)**,通过 DesireCore 本地 REST API(由内置工具 `MailOperations` 调用)操作邮件系统。支持 Gmail(OAuth2)、Outlook(MSAL)和 IMAP/SMTP(QQ、163、Yahoo 等)三种邮箱类型,涵盖收发邮件、搜索、标签管理、附件下载、草稿管理和自动规则。
### 使用场景 ### 使用场景
@@ -40,11 +40,12 @@ mail-operations 是一个**流程型技能(Procedural Skill)**,通过 Desi
## API 基础信息 ## API 基础信息
- **Base URL**: `https://127.0.0.1:62000/api` - **调用方式**:使用内置工具 `MailOperations`,参数为 `path`(以 `/api/` 开头的相对路径,查询参数直接拼在 path 里)、`method`(默认 `GET`)、`body`(JSON 对象)和 `save_to`(仅下载附件时使用,见第 6 节)。工具自动定位本机 Mail Service 并携带访问凭证,Agent 不需要、也不能自行提供 token。
- **认证**: 无需认证(本地服务) - **路径写法**:下文端点均省略 `/api` 前缀,调用时要补上,例如 `/imap/messages?email=me%40example.com&limit=20` 对应 `path: "/api/imap/messages?email=me%40example.com&limit=20"`。唯一例外是健康检查 `/ping`(不带 `/api`)。
- **Content-Type**: `application/json` - **不要直连端口**:不要用 `curl` 访问 `https://127.0.0.1:62000`。端口从 62000 起动态分配(同时运行多个 DesireCore 实例时,62000 可能属于另一个实例),严格能力模式下不带凭证的直连请求一律返回 401。
- **SSL**: 使用 `curl -k` 跳过自签名证书验证 - **参数位置**:GET / DELETE 的参数全部放查询串;POST / PUT 按下文表格区分查询串与 body。`messages/fetch`、标记已读/未读、Outlook 单封操作、IMAP 草稿发送等端点的 `email` 必须放在查询串,放进 body 会返回 400 `Missing email parameter`。`/sync` 的 `provider` 与 `email` 都必须放查询串:`provider` 放进 body 返回 400 `Invalid provider`,只有 `email` 放进 body 则不会报参数错误,但同步不会生效。
- **响应格式**: 成功 `{"code": 1, "msg": "Success", "result": ...}`,失败 `{"code": 0, "msg": "错误信息"}` - **Content-Type**: `application/json`(传 `body` 时工具自动设置)
- **响应格式**: 成功 `{"code": 1, "msg": "Success", "result": ...}`,失败 `{"code": 0, "msg": "错误信息"}`,部分错误另带稳定错误码字段 `error`(如 `smtp_unverified`)
--- ---
@@ -52,14 +53,14 @@ mail-operations 是一个**流程型技能(Procedural Skill)**,通过 Desi
以下规则优先级最高,每次操作必须遵守。 以下规则优先级最高,每次操作必须遵守。
### 规则 1:只能通过本地 API 操作 ### 规则 1:只能通过 MailOperations 工具操作
所有邮件操作**必须且只能**通过 `https://127.0.0.1:62000` 完成。**禁止**调用外部邮件客户端或浏览器。 所有邮件操作**必须且只能**通过内置工具 `MailOperations` 调用本机 Mail Service 完成。**禁止**调用外部邮件客户端或浏览器,也不要用 `curl` / `HttpRequest` 直连本机端口(`MailOperations` 错误提示中明确给出的大附件流式落盘办法除外)。
如果 API 返回 **401(授权过期)**: 如果返回 **401**,先按 `msg` 区分原因:
1. 告知用户该账户授权已失效 1. Gmail / Outlook 返回 `Not authorized` 或 `Authorization expired`:告知用户该账户授权已失效,引导用户在 DesireCore 邮箱页面重新授权。如需代为发起,调用 Gmail `POST /api/gmail/auth/initiate?loginHint={email}` 或 Outlook `POST /api/outlook/auth/initiate`,把返回的 `result.authUrl` 交给用户在浏览器中打开(服务不会自动打开浏览器)
2. 提示在 DesireCore 中重新授权 2. IMAP 返回 `Account not configured`:账户未配置或配置已损坏,与授权过期无关,需要用户重新添加账户(先 `/imap/test`,再 `/imap/accounts`)
3. Gmail: `POST /api/gmail/auth/initiate?loginHint={email}`,Outlook: `POST /api/outlook/auth/initiate` 3. 返回 `error: "unauthorized"`(缺少或无效的 Mail Service capability):说明请求没有经过 `MailOperations`,改用该工具重试
### 规则 2:操作前先确认账户 ### 规则 2:操作前先确认账户
@@ -71,7 +72,7 @@ mail-operations 是一个**流程型技能(Procedural Skill)**,通过 Desi
### 规则 3:查询为空时自动同步 ### 规则 3:查询为空时自动同步
查询返回空列表或找不到指定数据时: 查询返回空列表或找不到指定数据时:
1. 调用 `POST /{provider}/messages/fetch` 同步远程数据 1. 调用 `POST /{provider}/messages/fetch?email={email}` 同步远程数据(`email` 放查询串)
2. **自动重试**原查询 2. **自动重试**原查询
3. 仍为空才告知用户 3. 仍为空才告知用户
@@ -87,18 +88,20 @@ mail-operations 是一个**流程型技能(Procedural Skill)**,通过 Desi
|------|-------|---------|------| |------|-------|---------|------|
| 授权 | OAuth2 | OAuth2 (MSAL) | 密码/授权码 | | 授权 | OAuth2 | OAuth2 (MSAL) | 密码/授权码 |
| Provider 路径 | `/gmail/` | `/outlook/` | `/imap/` | | Provider 路径 | `/gmail/` | `/outlook/` | `/imap/` |
| 邮件详情 | 路径参数 `/{id}` | 查询参数 `?id={id}` | UID `/{uid}?folder=` | | 邮件详情 | 路径参数 `/{id}` | 查询参数 `?id={id}` | 路径参数 `/{uid}`(列表返回的 `imap:<uid>` 或纯数字 UID)+ `?folder=` |
| 搜索 | 支持 | 不支持 | 不支持 | | 搜索(本地缓存) | 支持 | 支持 | 支持 |
| 草稿 | 支持(含列表/详情) | 支持(创建/发送) | 支持(创建/发送) | | 远程同步翻页 | `pageToken`(不支持 `offset`) | `skip` / `offset` | `offset` |
| 草稿 | 支持(含列表/详情) | 支持(创建/更新/删除/发送) | 支持(创建/更新/删除/发送) |
| 附件下载 | 支持 | 支持 | 支持 | | 附件下载 | 支持 | 支持 | 支持 |
| 标签/分类 | 原生标签 | Categories | 仅本地标签 | | 标签/分类 | 原生标签 | Categories | 仅本地标签 |
| 发信前提 | 已授权 | 已授权 | SMTP 已验证(`smtpStatus: verified`) |
| 自动规则 | 支持 | 支持 | 支持 | | 自动规则 | 支持 | 支持 | 支持 |
--- ---
## 核心操作 ## 核心操作
以下 `{p}` 代表 provider(`gmail`、`outlook`、`imap`),`{email}` 需 URL 编码。 以下 `{p}` 代表 provider(`gmail`、`outlook`、`imap`),`{email}` 需 URL 编码(`@` → `%40`,`+` → `%2B`)。
### 1. 账户管理 ### 1. 账户管理
@@ -116,31 +119,41 @@ mail-operations 是一个**流程型技能(Procedural Skill)**,通过 Desi
| 操作 | 方法 | 端点 | | 操作 | 方法 | 端点 |
|------|------|------| |------|------|------|
| 获取预设邮箱配置 | GET | `/imap/presets` — 返回 QQ/163/Yahoo 等服务器配置 | | 获取预设邮箱配置 | GET | `/imap/presets` — 返回 QQ/163/Yahoo 等服务器配置 |
| 测试 IMAP 连接 | POST | `/imap/test` — body: `{email, password, imap: {host, port, secure}, smtp: {host, port, secure}}` | | 测试 IMAP 连接 | POST | `/imap/test` — body: `{email, password, imap?: {host, port, secure}, smtp?: {host, port, secure}}`;域名在预设中时 `imap` / `smtp` 可省略。**连接失败也返回 HTTP 200**,须检查 `result.imap.success` 与 `result.smtp.success` |
| 添加 IMAP 账户 | POST | `/imap/accounts` — body 同上,加 `displayName` | | 添加 IMAP 账户 | POST | `/imap/accounts` — body 同上,另可加 `displayName`、`allowUnverifiedSmtp`;成功返回 201 与 `{email, smtpStatus, smtp}`。SMTP 测试失败时默认返回 400(`error: smtp_verification_failed`),只有用户同意「仅收信」时才传 `allowUnverifiedSmtp: true` |
| 重新验证 SMTP | POST | `/imap/accounts/verify-smtp` — body: `{email}`;验证失败也返回 200,要看 `result.smtp.success` 与 `result.smtpStatus`:通过时 `smtpStatus` 恢复为 `verified`,失败时需要用户更新授权码后再验证 |
### 2. 邮件列表与同步 ### 2. 邮件列表与同步
| 操作 | 方法 | 端点 | 参数 | | 操作 | 方法 | 端点 | 参数 |
|------|------|------|------| |------|------|------|------|
| 查询本地缓存 | GET | `/{p}/messages` | `email, offset, limit, folder` | | 查询本地缓存 | GET | `/{p}/messages` | 查询串:`email, offset, limit, folder`;返回 `{messages, total, offset, limit}` |
| 远程同步 | POST | `/{p}/messages/fetch` | `email, limit, folder` | | 远程同步 | POST | `/{p}/messages/fetch?email=` | `email` 必须放查询串;body(或查询串):`folder`、`limit`(1–1000,默认 20);翻页:IMAP `offset`、Outlook `skip` / `offset`、Gmail `pageToken`(Gmail 传 `offset` 返回 400);返回 `messages` 与分页信息 `pagination` |
| 手动触发同步 | POST | `/sync` | `provider, email` | | 手动触发同步 | POST | `/sync?provider=&email=` | 参数放查询串;**仅支持 gmail / outlook**,IMAP 返回 400,请改用 `messages/fetch` |
**folder 取值**:Gmail/Outlook: `inbox, sent, drafts, trash, spam, archive`;IMAP: `INBOX, Sent, Drafts, Trash` 等。 **IMAP 远程同步较慢**:每封邮件都要下载并解析完整内容(实测约 0.65 封/秒),单批 `limit` 建议不超过 100;更早的邮件用 `offset`(跳过最新的多少封)分批拉取。
**folder 取值**:
- 本地缓存列表与搜索:`inbox, sent, drafts, trash, spam, archive, other`,大小写不敏感,也接受服务器原名(如 `INBOX`、`Sent Messages`);IMAP 的自定义文件夹在本地统一归为 `other`。
- 远程同步:通用名(`inbox`、`sent` 等)会自动映射到 IMAP 实际文件夹、Outlook 文件夹或 Gmail 标签;IMAP 也可直接传 `GET /imap/folders` 返回的 `path`。Gmail 的 `archive` 没有对应标签,会不加过滤地拉取。
- **IMAP 单封操作**(详情未命中缓存、标记已读/未读、删除、回复、附件下载)的 `folder` 会原样交给 IMAP 服务器,必须是 `GET /imap/folders` 返回的真实 `path`(如 `INBOX`、`Sent Messages`),省略时默认 `INBOX`。列表项里的 `folder` 是归一化后的值(如 `sent`),不能直接当作 IMAP 文件夹名。
**响应格式**(邮件列表项): **响应格式**(邮件列表项):
```json ```json
{ {
"id": "消息ID", "subject": "主题", "id": "消息ID(IMAP 为 imap:<uid>)", "provider": "gmail",
"subject": "主题",
"from": {"name": "...", "address": "..."}, "from": {"name": "...", "address": "..."},
"to": [{"name": "...", "address": "..."}], "toRecipients": [{"name": "...", "address": "..."}],
"date": "ISO8601", "snippet": "摘要", "receivedDateTime": "ISO8601", "sentDateTime": "ISO8601",
"bodyPreview": "摘要",
"isRead": true, "hasAttachments": false, "isRead": true, "hasAttachments": false,
"labels": ["INBOX"], "folder": "inbox" "folder": "inbox", "labelIds": ["INBOX"]
} }
``` ```
`labelIds` 只在 Gmail 邮件中出现,`categories` 只在 Outlook 邮件中出现。
### 3. 单封邮件操作 ### 3. 单封邮件操作
| 操作 | Gmail | Outlook | IMAP | | 操作 | Gmail | Outlook | IMAP |
@@ -150,9 +163,9 @@ mail-operations 是一个**流程型技能(Procedural Skill)**,通过 Desi
| 标记未读 | POST `/{id}/unread?email=` | POST `/message/unread?id={id}&email=` | POST `/{uid}/unread?email=&folder=` | | 标记未读 | POST `/{id}/unread?email=` | POST `/message/unread?id={id}&email=` | POST `/{uid}/unread?email=&folder=` |
| 删除 | DELETE `/{id}?email=` | DELETE `/message?id={id}&email=` | DELETE `/{uid}?email=&folder=` | | 删除 | DELETE `/{id}?email=` | DELETE `/message?id={id}&email=` | DELETE `/{uid}?email=&folder=` |
> 所有路径前缀为 `/api/{provider}/messages`(Gmail/IMAP)或 `/api/outlook/`(Outlook 特殊路由)。 > 所有路径前缀为 `/api/{provider}/messages`(Gmail/IMAP)或 `/api/outlook/`(Outlook 特殊路由)。IMAP 的 `{uid}` 可直接用列表返回的 `id`(`imap:<uid>`),`folder` 规则见第 2 节,INBOX 可省略。
**邮件详情额外字段**:`body: {content, contentType}`, `cc`, `attachments: [{id, filename, mimeType, size}]` **邮件详情额外字段**:`body: {content, contentType}`, `ccRecipients`, `attachments: [{id, filename, contentType, size}]`。IMAP 附件的 `id` 是附件在该邮件中的序号字符串(`"0"`、`"1"`……)。
### 4. 发送与回复 ### 4. 发送与回复
@@ -170,26 +183,35 @@ mail-operations 是一个**流程型技能(Procedural Skill)**,通过 Desi
} }
``` ```
`attachments` 每项为 `{filename, content(base64), contentType}`。Gmail 要求至少一个非空收件人。IMAP 账户的 `smtpStatus` 为 `unverified`(仅收信接入)时,发送、回复、发送草稿均返回 409(`error: smtp_unverified`),需先引导用户重新验证 SMTP。
**回复邮件**: **回复邮件**:
| Provider | 端点 | Body | | Provider | 端点 | Body |
|----------|------|------| |----------|------|------|
| Gmail | POST `/gmail/reply` | `{email, messageId, body, contentType}` | | Gmail | POST `/gmail/reply` | `{email, messageId, body, contentType}` |
| Outlook | POST `/outlook/message/reply?id={id}&email=` | `{body, contentType}` | | Outlook | POST `/outlook/message/reply?id={id}&email=` | `{body, contentType}` |
| IMAP | POST `/imap/reply` | `{email, uid, folder, body, contentType}` | | IMAP | POST `/imap/reply` | `{email, uid, folder, body, contentType}`(`uid` 可用 `imap:<uid>`,`folder` 默认 `INBOX`) |
### 5. 搜索(仅 Gmail) ### 5. 搜索(三种邮箱均支持)
`GET /api/gmail/search?email={email}&q={keyword}` `GET /api/{p}/search?email={email}&q={keyword}`(`{p}` 为 `gmail`、`outlook` 或 `imap`)
搜索只查**本地缓存**,不访问邮件服务器;搜不到时先按规则 3 远程同步再搜。
| 参数 | 说明 | | 参数 | 说明 |
|------|------| |------|------|
| `q` | 关键词(搜索主题、正文、发件人) | | `q` | 关键词,**只匹配主题**(大小写不敏感) |
| `from` | 发件人地址 | | `from` | 发件人地址(包含匹配) |
| `dateFrom` / `dateTo` | 日期范围 YYYY-MM-DD | | `dateFrom` / `dateTo` | 日期范围 YYYY-MM-DD(按本机时区,含首尾两天) |
| `hasAttachment` | true/false | | `hasAttachment` | `true` 时只返回带附件的邮件(`false` 不做筛选) |
| `isUnread` | true/false | | `isUnread` | `true` 时只返回未读邮件(`false` 不做筛选) |
| `offset` / `limit` | 分页 | | `folder` | 文件夹过滤,取值同本地缓存列表 |
| `offset` / `limit` | 分页,`limit` 最大 200 |
参数名必须完全一致,未知参数(如 `query`、`hasAttachments`)直接返回 400。返回 `{messages, total, offset, limit}`。
需要按正文或发件人在 Gmail 服务器上全文检索时,改用 `POST /gmail/messages/fetch?email=` 的 `query` 参数:它原样交给 Gmail API,支持 Gmail 搜索语法(如 `from:`、`has:attachment`),结果同时写入本地缓存。Outlook 与 IMAP 没有对应的远程检索参数。较旧的客户端若对 `/outlook/search` 或 `/imap/search` 返回 404,改为分页读取 `GET /{p}/messages` 后自行筛选。
### 6. 附件下载 ### 6. 附件下载
@@ -197,9 +219,23 @@ mail-operations 是一个**流程型技能(Procedural Skill)**,通过 Desi
|----------|------|------|------| |----------|------|------|------|
| Gmail | POST | `/gmail/messages/{messageId}/attachment` | `{email, attachmentId}` | | Gmail | POST | `/gmail/messages/{messageId}/attachment` | `{email, attachmentId}` |
| Outlook | POST | `/outlook/attachment` | `{email, messageId, attachmentId}` | | Outlook | POST | `/outlook/attachment` | `{email, messageId, attachmentId}` |
| IMAP | POST | `/imap/attachment` | `{email, uid, folder, partId}` | | IMAP | POST | `/imap/attachment` | `{email, messageId, attachmentId, folder?}` |
响应 `result.data` 为 base64 编码,解码后保存文件。 - `attachmentId` 取自邮件详情的 `attachments[].id`,先调用详情接口拿到附件列表。
- IMAP:`messageId` 直接用列表返回的 `id`(如 `imap:12345`);`attachmentId` 是附件序号字符串(第一个附件为 `"0"`);`folder` 为服务器上的真实文件夹路径,INBOX 可省略。
- **下载附件时必须给 `MailOperations` 传 `save_to`**(目标文件路径):工具在进程内完成 base64 解码并写入文件,只返回绝对路径、字节数与 SHA-256;不传 `save_to` 时附件响应会被工具拒绝返回。目标路径须位于允许写入的工作目录内。
- 端点原始响应为 `result: {data: <base64>, size}`;响应超过约 32MB 时工具会拒绝解析,此时按工具错误提示中的流式落盘办法处理,或请用户在邮箱页面手工导出附件。
示例(IMAP):
```json
{
"path": "/api/imap/attachment",
"method": "POST",
"body": {"email": "me@example.com", "messageId": "imap:12345", "attachmentId": "0"},
"save_to": "./attachments/report.pdf"
}
```
> Gmail 使用 POST 因为 attachmentId 可能超出 URL 长度限制。 > Gmail 使用 POST 因为 attachmentId 可能超出 URL 长度限制。
@@ -211,7 +247,7 @@ mail-operations 是一个**流程型技能(Procedural Skill)**,通过 Desi
|------|------|------| |------|------|------|
| 获取草稿列表 | GET | `/gmail/drafts?email=&limit=` | | 获取草稿列表 | GET | `/gmail/drafts?email=&limit=` |
| 获取草稿详情 | GET | `/gmail/drafts/{draftId}?email=` | | 获取草稿详情 | GET | `/gmail/drafts/{draftId}?email=` |
| 创建草稿 | POST | `/gmail/drafts` — body: `{email, to, cc, subject, body, contentType}` | | 创建草稿 | POST | `/gmail/drafts` — body: `{email, to, cc, subject, body, contentType}`(`to` / `cc` 是逗号分隔的地址字符串) |
| 更新草稿 | PUT | `/gmail/drafts/{draftId}` — body 同创建 | | 更新草稿 | PUT | `/gmail/drafts/{draftId}` — body 同创建 |
| 删除草稿 | DELETE | `/gmail/drafts/{draftId}?email=` | | 删除草稿 | DELETE | `/gmail/drafts/{draftId}?email=` |
@@ -228,10 +264,10 @@ mail-operations 是一个**流程型技能(Procedural Skill)**,通过 Desi
| 操作 | 方法 | 端点 | | 操作 | 方法 | 端点 |
|------|------|------| |------|------|------|
| 创建草稿 | POST | `/imap/drafts` — body: `{email, toRecipients, subject, body, contentType}` | | 创建草稿 | POST | `/imap/drafts` — body: `{email, to, cc, subject, body, contentType}`(`to` / `cc` 是逗号分隔的地址字符串,不是 `toRecipients` 数组);返回 `{uid, id}` |
| 更新草稿 | PUT | `/imap/drafts/{uid}` — body: `{email, folder, toRecipients, subject, body, contentType}` | | 更新草稿 | PUT | `/imap/drafts/{uid}` — body 同创建;实现是删掉旧草稿再新建,返回**新的** `{uid, id}`,之后发送或删除要用新 uid(拿旧 uid 会得到 404 `Draft not found`)。服务器不支持 UIDPLUS 时返回的 uid 为 0,此时先重新读取草稿列表再操作 |
| 删除草稿 | DELETE | `/imap/drafts/{uid}?email=&folder=` | | 删除草稿 | DELETE | `/imap/drafts/{uid}?email=` |
| 发送草稿 | POST | `/imap/drafts/{uid}/send` — body: `{email, folder}` | | 发送草稿 | POST | `/imap/drafts/{uid}/send?email=` — `email` 放查询串,无需 body |
### 8. 标签管理(统一接口) ### 8. 标签管理(统一接口)
@@ -246,7 +282,9 @@ mail-operations 是一个**流程型技能(Procedural Skill)**,通过 Desi
| 添加邮件标签 | POST | `/mails/{p}/{email}/labels?mailId=` — body: `{"labelId": "..."}` | | 添加邮件标签 | POST | `/mails/{p}/{email}/labels?mailId=` — body: `{"labelId": "..."}` |
| 批量设置标签 | PUT | `/mails/{p}/{email}/labels?mailId=` — body: `{"labelIds": [...]}` | | 批量设置标签 | PUT | `/mails/{p}/{email}/labels?mailId=` — body: `{"labelIds": [...]}` |
| 移除邮件标签 | DELETE | `/mails/{p}/{email}/labels?mailId=&labelId=` | | 移除邮件标签 | DELETE | `/mails/{p}/{email}/labels?mailId=&labelId=` |
| 获取标签下邮件 | GET | `/labels/{labelId}/mails?provider=&email=&limit=&offset=` | | 获取标签下邮件 | GET | `/labels/{labelId}/mails?provider=&email=&limit=&offset=` — 返回 `mailIds`(不含邮件内容) |
统一标签接口只修改 DesireCore 本地数据,不会同步到邮件服务器。需要改动服务器侧标签时,使用下方 Gmail 原生标签或第 9 节 Outlook 分类。
**Gmail 原生标签**: **Gmail 原生标签**:
- 获取标签列表:`GET /api/gmail/labels?email=` - 获取标签列表:`GET /api/gmail/labels?email=`
@@ -290,46 +328,47 @@ Outlook 使用 Categories 而非 Labels。
"email": "xxx@gmail.com", "email": "xxx@gmail.com",
"enabled": true, "enabled": true,
"conditions": [ "conditions": [
{"field": "from|to|subject|body|has_attachment", "operator": "contains|equals|regex|...", "value": "..."} {"field": "from|to|subject|body|has_attachment", "operator": "contains|not_contains|equals|not_equals|starts_with|ends_with|matches_regex|is_true|is_false", "value": "..."}
], ],
"conditionLogic": "and", "conditionLogic": "and",
"actions": [ "actions": [
{"type": "add_label|remove_label|mark_as_read|mark_as_unread|archive|delete|auto_reply|agent_handle", "value": "..."} {"type": "add_label|remove_label|mark_as_read|mark_as_unread|delete|auto_reply|agent_handle", "value": "..."}
], ],
"priority": 1, "priority": 1,
"stopOnMatch": false "stopOnMatch": false
} }
``` ```
正则条件的 operator 是 `matches_regex`(写成 `regex` 会被当作未知操作符,永远不匹配);`is_true` / `is_false` 只用于 `has_attachment`。
**动作类型说明**: **动作类型说明**:
| type | value | 说明 | | type | value | 说明 |
|------|-------|------| |------|-------|------|
| `add_label` | 标签 ID | 添加标签 | | `add_label` | 标签 ID | 添加本地标签 |
| `remove_label` | 标签 ID | 移除标签 | | `remove_label` | 标签 ID | 移除本地标签 |
| `mark_as_read` | 省略 | 标记已读 | | `mark_as_read` | 省略 | 仅在本地缓存中标记已读 |
| `mark_as_unread` | 省略 | 标记未读 | | `mark_as_unread` | 省略 | 仅在本地缓存中标记未读 |
| `archive` | 省略 | 归档 | | `delete` | 省略 | 仅从本地缓存删除,服务器上的邮件不受影响 |
| `delete` | 省略 | 删除 |
| `auto_reply` | 回复文本 | 自动回复固定内容 | | `auto_reply` | 回复文本 | 自动回复固定内容 |
| `agent_handle` | Agent ID | 触发智能体处理邮件 | | `agent_handle` | Agent ID | 触发智能体处理邮件 |
> 规则在轮询引擎检测到新邮件时**自动执行**,无需手动调用。`auto_reply` 和 `agent_handle` 支持全部三种邮箱类型。 > 规则在轮询引擎检测到新邮件时**自动执行**,无需手动调用(IMAP 只覆盖收件箱的新邮件)。`auto_reply` 和 `agent_handle` 支持全部三种邮箱类型;`agent_handle` 在创建或更新规则时绑定当时的 Agent Service 连接。`archive`、`move_to_folder`、`forward_to`、`star` 可以保存,但当前执行时不做任何操作,不要向用户承诺这些效果。
### 11. 授权管理 ### 11. 授权管理
| 操作 | 方法 | 端点 | | 操作 | 方法 | 端点 |
|------|------|------| |------|------|------|
| Gmail OAuth | POST | `/gmail/auth/initiate?loginHint={email}` — 打开浏览器授权 | | Gmail OAuth | POST | `/gmail/auth/initiate?loginHint={email}` — 返回 `result.authUrl`,由用户在浏览器中打开完成授权 |
| Gmail 状态 | GET | `/gmail/auth/status?email=` | | Gmail 状态 | GET | `/gmail/auth/status?email=` |
| Outlook OAuth | POST | `/outlook/auth/initiate` — 打开浏览器授权 | | Outlook OAuth | POST | `/outlook/auth/initiate` — 返回 `result.authUrl`,由用户在浏览器中打开完成授权 |
| Outlook 状态 | GET | `/outlook/auth/status?email=` | | Outlook 状态 | GET | `/outlook/auth/status?email=` |
### 12. 文件夹 ### 12. 文件夹
| 操作 | 方法 | 端点 | | 操作 | 方法 | 端点 |
|------|------|------| |------|------|------|
| IMAP 文件夹列表 | GET | `/imap/folders?email=` | | IMAP 文件夹列表 | GET | `/imap/folders?email=` — 返回 `[{name, path, specialUse, flags}]`,IMAP 单封操作的 `folder` 使用其中的 `path` |
| Outlook 文件夹列表 | GET | `/outlook/folders?email=` | | Outlook 文件夹列表 | GET | `/outlook/folders?email=` |
> Gmail 文件夹固定:inbox, sent, drafts, trash, spam, archive。 > Gmail 文件夹固定:inbox, sent, drafts, trash, spam, archive。
@@ -340,11 +379,11 @@ Outlook 使用 Categories 而非 Labels。
邮件系统采用**本地缓存 + 定期轮询**: 邮件系统采用**本地缓存 + 定期轮询**:
- **写操作**(发送、标记、删除、标签):同时更新本地和远程,无延迟 - **写操作**(发送、标记、删除、Gmail 原生标签、Outlook 分类):通过对应 provider 端点同时更新本地和远程,无延迟;统一标签接口(第 8 节)和自动规则中的标记、删除、标签动作只修改本地
- **读操作**(查询、搜索):返回本地缓存,可能有延迟(默认 30 秒轮询) - **读操作**(查询、搜索):返回本地缓存,可能有延迟(默认 30 秒轮询)
- **远程变更**(用户在官方页面操作):需等待下次轮询同步 - **远程变更**(用户在官方页面操作):Gmail / Outlook 需等待下次轮询同步;IMAP 轮询只拉取收件箱的新邮件,其他文件夹需主动调用 `messages/fetch`,服务器上对已缓存邮件做的已读、删除等变更不会回写到本地
**存储路径**:`${DESIRECORE_ROOT}/mail/{provider}/{email}/`(index.json, messages/, sync.json) **存储路径**:`${DESIRECORE_ROOT}/mail/{provider}/{email}/`(meta.json, index.json, messages/)
--- ---
@@ -352,9 +391,10 @@ Outlook 使用 Categories 而非 Labels。
| 状态码 | 原因 | 处理 | | 状态码 | 原因 | 处理 |
|--------|------|------| |--------|------|------|
| 400 | 参数错误 | 检查请求参数 | | 400 | 参数错误(包括参数放错位置,如把查询串参数放进 body) | 按 `msg` 检查参数名与位置 |
| 401 | 授权过期 | **按规则 1 处理**,不要尝试其他途径 | | 401 | Gmail/Outlook 授权失效、IMAP 账户未配置,或缺少 Mail Service capability | **按规则 1 处理**,不要尝试其他途径 |
| 404 | 资源不存在 | 先同步再重试(规则 3) | | 404 | 资源不存在 | 先同步再重试(规则 3) |
| 500 | 内部错误 | 告知用户稍后重试 | | 409 | IMAP 账户 SMTP 未验证(`error: smtp_unverified`) | 引导用户重新验证 SMTP(`POST /imap/accounts/verify-smtp`) |
| 500 | 内部错误(IMAP 常见原因:`folder` 不是服务器上存在的文件夹) | 先检查 `folder`,仍失败则告知用户稍后重试 |
**IMAP 注意**:国内邮箱(QQ、163)需使用"授权码"而非登录密码。用 `/imap/test` 预先验证配置。 **IMAP 注意**:国内邮箱(QQ、163)需使用"授权码"而非登录密码。用 `/imap/test` 预先验证配置,并检查返回的 `result.imap.success` 与 `result.smtp.success`。

View File

@@ -31,14 +31,14 @@
}, },
"release": { "release": {
"state": "known", "state": "known",
"version": "1.0.5", "version": "1.0.6",
"versionScheme": "semver" "versionScheme": "semver"
}, },
"timestamps": { "timestamps": {
"catalogUpdatedAt": { "catalogUpdatedAt": {
"state": "known", "state": "known",
"value": "2026-07-07T13:14:31Z", "value": "2026-09-29",
"precision": "second" "precision": "day"
}, },
"releasePublishedAt": { "releasePublishedAt": {
"state": "unknown" "state": "unknown"
@@ -67,7 +67,8 @@
"compatibility": { "compatibility": {
"platforms": { "platforms": {
"state": "unknown" "state": "unknown"
} },
"requiredClientVersion": "10.0.148"
}, },
"spec": { "spec": {
"kind": "skill", "kind": "skill",

View File

@@ -1,12 +1,14 @@
# 离线队列 API # 离线队列 API
当网络不可用时,写操作会进入离线队列,恢复网络后自动处理。 离线队列目前只能通过下列接口手动登记和处理:写操作失败时**不会**自动入队,网络恢复后也**不会**自动处理。`POST /offline/process` 执行时,`mark_as_read`、`mark_as_unread`、`delete`、`add_label`、`remove_label` 只修改本地缓存,不同步到邮件服务器;`send_email`、`archive`、`move_to_folder` 尚未实现。这些未实现的类型、以及缺少 `params.labelId` 的加/去标签操作,处理时什么都不做却照样出队并计入 `processed`——`processed` 的计数不代表操作真的执行了。不要把离线队列当作发信或同步远程的手段。
下表端点与主文档一样省略 `/api` 前缀,经 `MailOperations` 调用时需补上(`/ping` 除外)。
| 操作 | 方法 | 端点 | | 操作 | 方法 | 端点 |
|------|------|------| |------|------|------|
| 获取队列状态 | GET | `/offline/status` | | 获取队列状态 | GET | `/offline/status` |
| 获取队列操作 | GET | `/offline/queue` | | 获取队列操作 | GET | `/offline/queue` |
| 添加操作到队列 | POST | `/offline/queue` — body: `{type, provider, email, mailId, params}` | | 添加操作到队列 | POST | `/offline/queue` — body: `{type, provider, email, mailId, params}`,`type` ∈ `mark_as_read, mark_as_unread, delete, move_to_folder, add_label, remove_label, send_email, archive` |
| 移除单个操作 | DELETE | `/offline/queue/{operationId}` | | 移除单个操作 | DELETE | `/offline/queue/{operationId}` |
| 清空队列 | DELETE | `/offline/queue` | | 清空队列 | DELETE | `/offline/queue` |
| 清除失败操作 | DELETE | `/offline/failed` | | 清除失败操作 | DELETE | `/offline/failed` |