feat(api): 升级开放 API 并支持投递回调
- 为 `/api/open/v1` 引入 Token scope、分页游标、幂等发信、发送事件与重试/取消能力。 - 新增投递事件签名回调与状态 webhook outbox,补充相关配置、迁移和测试。 - 同步更新 Web 端 API 类型、个人中心 Token 权限管理,以及中英文文档和 OpenAPI 契约。
This commit is contained in:
+96
-41
@@ -1,8 +1,12 @@
|
||||
# LanQin Email API
|
||||
|
||||
LanQin Email exposes integration-oriented APIs under `/api/open`.
|
||||
LanQin Email exposes versioned integration APIs under `/api/open/v1`. The original `/api/open` paths remain compatibility aliases.
|
||||
|
||||
这些接口用于外部系统集成,统一放在 `/api/open` 下。它们不是匿名公开接口,只接受 API Token,不接受浏览器登录 Session Cookie。
|
||||
这些接口用于外部系统集成,稳定版本入口为 `/api/open/v1`。原 `/api/open` 路径继续作为兼容别名。它们不是匿名公开接口,只接受 API Token,不接受浏览器登录 Session Cookie。
|
||||
|
||||
Machine-readable OpenAPI 3.1 contract: [`docs/openapi.json`](./openapi.json).
|
||||
|
||||
机器可读的 OpenAPI 3.1 契约见 [`docs/openapi.json`](./openapi.json)。
|
||||
|
||||
## Base URL
|
||||
|
||||
@@ -28,6 +32,7 @@ The API uses standard HTTP status codes:
|
||||
| `401 Unauthorized` | Missing or invalid API token / 缺少或无效的 API Token |
|
||||
| `403 Forbidden` | Token lacks required permissions / Token 缺少所需权限 |
|
||||
| `404 Not Found` | Resource does not exist / 资源不存在 |
|
||||
| `409 Conflict` | Idempotency key conflict or concurrent status change / 幂等键冲突或状态并发变化 |
|
||||
| `429 Too Many Requests` | Rate limit exceeded / 超过频率限制 |
|
||||
| `500 Internal Server Error` | Server error / 服务器错误 |
|
||||
|
||||
@@ -101,27 +106,32 @@ Tokens created without a custom expiration default to 90 days. You can disable o
|
||||
|
||||
如果没有自定义到期时间,Token 默认 90 天后过期。你可以在同一个个人中心页面中禁用或撤销 Token。
|
||||
|
||||
Each token has independent scopes. Scopes only reduce the permissions of the owning user; they never grant permissions the user does not already have. Existing tokens created before scope support are migrated to `*` for compatibility.
|
||||
|
||||
每个 Token 都有独立 scope。scope 只会收缩 Token 所属用户已有的权限,不会授予用户原本没有的权限。scope 功能上线前创建的 Token 会迁移为 `*`,以保持兼容。
|
||||
|
||||
| Scope | Purpose |
|
||||
|---|---|
|
||||
| `domains:read` / `domains:write` | View or manage sending domains |
|
||||
| `mailboxes:read` / `mailboxes:write` | View or manage mailboxes; password reset is a write operation |
|
||||
| `messages:read` / `messages:send` / `messages:manage` | Read messages/status, send, or retry/cancel |
|
||||
| `aliases:read` / `aliases:write` | View or manage aliases |
|
||||
| `dns:read` / `dns:check` | View required records or execute DNS checks |
|
||||
| `*` | Compatibility wildcard; avoid for new integrations |
|
||||
|
||||
## Permissions
|
||||
|
||||
All Open API endpoints require an API token with appropriate permissions and role requirements:
|
||||
|
||||
所有 Open API 接口都需要具备相应权限和角色的 API Token:
|
||||
|
||||
| Endpoint 接口 | Required Permission 所需权限 | Required Role 所需角色 |
|
||||
|----------|-------------------|---------------|
|
||||
| `GET /api/open/domains` | any of `admin.domains.view`, `admin.dns.view`, `admin.mailboxes.view`, `admin.aliases.view`, `admin.settings.view`, `admin.templates.view` | admin |
|
||||
| `POST /api/open/domains` | `admin.domains.create` | admin |
|
||||
| `GET /api/open/domains/{id}` | any of `admin.domains.view`, `admin.dns.view`, `admin.mailboxes.view`, `admin.aliases.view`, `admin.settings.view`, `admin.templates.view` | admin |
|
||||
| `POST /api/open/domains/{id}` | `admin.domains.update` | admin |
|
||||
| `DELETE /api/open/domains/{id}` | `admin.domains.delete` | admin |
|
||||
| `GET /api/open/mailboxes` | `admin.mailboxes.view` or `admin.messages.view` | admin |
|
||||
| `POST /api/open/mailboxes` | `admin.mailboxes.create` | admin |
|
||||
| `GET /api/open/mailboxes/{id}` | `admin.mailboxes.view` or `admin.messages.view` | admin |
|
||||
| `POST /api/open/mailboxes/{id}` | `admin.mailboxes.update` | admin |
|
||||
| `DELETE /api/open/mailboxes/{id}` | `admin.mailboxes.delete` | admin |
|
||||
| `POST /api/open/send` | `mail.messages.send` | user or admin |
|
||||
| `GET /api/open/send/{id}` | `mail.messages.read` | user or admin |
|
||||
| `GET /api/open/mailboxes/{id}/messages` | `mail.messages.read` | user or admin |
|
||||
| Endpoint group | Required scope | Role |
|
||||
|---|---|---|
|
||||
| Domains | `domains:read` or `domains:write` | admin |
|
||||
| Mailboxes | `mailboxes:read` or `mailboxes:write` | admin |
|
||||
| DNS | `dns:read` or `dns:check` | admin |
|
||||
| Aliases | `aliases:read` or `aliases:write` | admin |
|
||||
| Send / status / messages | `messages:send`, `messages:read`, or `messages:manage` | user or admin |
|
||||
|
||||
**Notes:**
|
||||
- Admin endpoints check for `requireAdminAccess` (role must be `admin`).
|
||||
@@ -138,7 +148,7 @@ All Open API endpoints require an API token with appropriate permissions and rol
|
||||
### List domains
|
||||
|
||||
```http
|
||||
GET /api/open/domains
|
||||
GET /api/open/v1/domains
|
||||
Authorization: Bearer lq_xxx
|
||||
```
|
||||
|
||||
@@ -182,7 +192,7 @@ Authorization: Bearer lq_xxx
|
||||
### Create domain
|
||||
|
||||
```http
|
||||
POST /api/open/domains
|
||||
POST /api/open/v1/domains
|
||||
Authorization: Bearer lq_xxx
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -221,7 +231,7 @@ Content-Type: application/json
|
||||
### Get domain
|
||||
|
||||
```http
|
||||
GET /api/open/domains/{id}
|
||||
GET /api/open/v1/domains/{id}
|
||||
Authorization: Bearer lq_xxx
|
||||
```
|
||||
|
||||
@@ -234,7 +244,7 @@ Authorization: Bearer lq_xxx
|
||||
### Update domain status
|
||||
|
||||
```http
|
||||
POST /api/open/domains/{id}
|
||||
POST /api/open/v1/domains/{id}
|
||||
Authorization: Bearer lq_xxx
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -262,7 +272,7 @@ Content-Type: application/json
|
||||
### Delete domain
|
||||
|
||||
```http
|
||||
DELETE /api/open/domains/{id}
|
||||
DELETE /api/open/v1/domains/{id}
|
||||
Authorization: Bearer lq_xxx
|
||||
```
|
||||
|
||||
@@ -289,7 +299,7 @@ Authorization: Bearer lq_xxx
|
||||
### List mailboxes
|
||||
|
||||
```http
|
||||
GET /api/open/mailboxes
|
||||
GET /api/open/v1/mailboxes
|
||||
Authorization: Bearer lq_xxx
|
||||
```
|
||||
|
||||
@@ -323,7 +333,7 @@ Authorization: Bearer lq_xxx
|
||||
### Create mailbox
|
||||
|
||||
```http
|
||||
POST /api/open/mailboxes
|
||||
POST /api/open/v1/mailboxes
|
||||
Authorization: Bearer lq_xxx
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -370,7 +380,7 @@ Content-Type: application/json
|
||||
### Get mailbox
|
||||
|
||||
```http
|
||||
GET /api/open/mailboxes/{id}
|
||||
GET /api/open/v1/mailboxes/{id}
|
||||
Authorization: Bearer lq_xxx
|
||||
```
|
||||
|
||||
@@ -383,7 +393,7 @@ Authorization: Bearer lq_xxx
|
||||
### Update mailbox
|
||||
|
||||
```http
|
||||
POST /api/open/mailboxes/{id}
|
||||
POST /api/open/v1/mailboxes/{id}
|
||||
Authorization: Bearer lq_xxx
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -408,7 +418,7 @@ All fields are optional. Omitted (or empty / non-positive) fields keep their cur
|
||||
### Delete mailbox
|
||||
|
||||
```http
|
||||
DELETE /api/open/mailboxes/{id}
|
||||
DELETE /api/open/v1/mailboxes/{id}
|
||||
Authorization: Bearer lq_xxx
|
||||
```
|
||||
|
||||
@@ -433,8 +443,9 @@ Authorization: Bearer lq_xxx
|
||||
## Send Mail
|
||||
|
||||
```http
|
||||
POST /api/open/send
|
||||
POST /api/open/v1/send
|
||||
Authorization: Bearer lq_xxx
|
||||
Idempotency-Key: invoice-2026-0001
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
@@ -505,7 +516,7 @@ Total attachment size is limited by the sender's permission group (`maxAttachmen
|
||||
|
||||
| Field 字段 | Description 说明 |
|
||||
|-------|-------------|
|
||||
| `id` | Send identifier; use it with `GET /api/open/send/{id}` / 发信标识,可配合 `GET /api/open/send/{id}` 使用 |
|
||||
| `id` | Send identifier; use it with `GET /api/open/v1/send/{id}` / 发信标识,可配合 `GET /api/open/v1/send/{id}` 使用 |
|
||||
| `queueId` | SMTP queue item id. Omitted when the message was only `accepted` / SMTP 队列项 ID;仅 `accepted` 时不返回 |
|
||||
| `status` | Delivery status, see values below / 投递状态,见下方取值 |
|
||||
| `messageId` | Internal stored message id / 内部存储的消息 ID |
|
||||
@@ -520,6 +531,10 @@ When SMTP delivery is not configured, the message can be stored as accepted with
|
||||
|
||||
如果没有配置 SMTP 投递,邮件可能只会进入 `accepted` 状态,不会产生 `queueId`。
|
||||
|
||||
`id` is always the stable stored send id (`mail_*`). `queueId` is the queue item (`snd_*`) and may be absent. A repeated request with the same `Idempotency-Key` and identical body returns the original send with `200` and `Idempotency-Replayed: true`; reusing the key with a different body returns `409`. Keys are retained for 24 hours.
|
||||
|
||||
`id` 始终是稳定的发送邮件 ID(`mail_*`);`queueId` 是队列项 ID(`snd_*`),可能不存在。相同 `Idempotency-Key` 与相同请求体重试时返回原发送结果、状态码 `200`,并带 `Idempotency-Replayed: true`;相同 key 配不同请求体返回 `409`。key 保留 24 小时。
|
||||
|
||||
Current status values:
|
||||
|
||||
当前状态取值:
|
||||
@@ -527,18 +542,22 @@ Current status values:
|
||||
- `accepted`: message was accepted and stored, but no SMTP queue item exists.
|
||||
- `queued`: queued for SMTP delivery.
|
||||
- `sending`: currently being delivered.
|
||||
- `delivered`: SMTP delivery succeeded.
|
||||
- `relayed`: the configured upstream SMTP server accepted the message; this is not final recipient delivery.
|
||||
- `failed`: delivery failed and may be retried.
|
||||
- `canceled`: delivery was canceled.
|
||||
- `delivered`, `bounced`, `complained`, `rejected`, `deferred`: final per-recipient provider/DSN event.
|
||||
- `partial`: final events currently differ between recipients or only cover part of the recipient list.
|
||||
|
||||
<br>
|
||||
|
||||
- `accepted`:邮件已被接受并存储,但没有 SMTP 队列项。
|
||||
- `queued`:已进入 SMTP 投递队列。
|
||||
- `sending`:正在投递中。
|
||||
- `delivered`:SMTP 投递成功。
|
||||
- `relayed`:配置的上游 SMTP 已接受邮件,但这不代表最终收件成功。
|
||||
- `failed`:投递失败,可能会重试。
|
||||
- `canceled`:投递已取消。
|
||||
- `delivered`、`bounced`、`complained`、`rejected`、`deferred`:每个收件人的最终供应商或 DSN 事件。
|
||||
- `partial`:不同收件人的最终状态不同,或当前只收到了部分收件人的事件。
|
||||
|
||||
**Error cases:**
|
||||
|
||||
@@ -550,31 +569,31 @@ Current status values:
|
||||
| `429` | SMTP send rate limit exceeded / 超过 SMTP 发信频率限制 |
|
||||
| `507` | Mailbox quota exceeded / 邮箱配额已满 |
|
||||
|
||||
Bounce, complaint, rejection, and provider-specific delivery events require future webhook or delivery-event integration.
|
||||
Final delivery events are exposed in `recipientStatuses` and through `GET /api/open/v1/send/{id}/events`.
|
||||
|
||||
退信、投诉、拒收等更细状态需要后续接入投递事件或 webhook 后才能完整提供。
|
||||
最终投递事件会出现在 `recipientStatuses`,完整时间线可通过 `GET /api/open/v1/send/{id}/events` 获取。
|
||||
|
||||
## Send Status
|
||||
|
||||
```http
|
||||
GET /api/open/send/{id}
|
||||
GET /api/open/v1/send/{id}
|
||||
Authorization: Bearer lq_xxx
|
||||
```
|
||||
|
||||
**Status:** `200 OK` or `404 Not Found`
|
||||
|
||||
`id` can be the value returned by `POST /api/open/send`. If a queue item exists, it can also be the queue id.
|
||||
`id` can be the value returned by `POST /api/open/v1/send`. If a queue item exists, it can also be the queue id.
|
||||
|
||||
`id` 可以使用发信接口返回的 `id`;如果存在队列项,也可以使用 `queueId`。
|
||||
|
||||
**Response:** Same shape as the `POST /api/open/send` response. Only messages belonging to the token user's mailboxes are returned; otherwise `404`.
|
||||
**Response:** Same shape as the `POST /api/open/v1/send` response. Only messages belonging to the token user's mailboxes are returned; otherwise `404`.
|
||||
|
||||
**响应:** 结构与 `POST /api/open/send` 的响应相同。只会返回属于 Token 拥有者邮箱的邮件,否则返回 `404`。
|
||||
**响应:** 结构与 `POST /api/open/v1/send` 的响应相同。只会返回属于 Token 拥有者邮箱的邮件,否则返回 `404`。
|
||||
|
||||
## Received Messages
|
||||
|
||||
```http
|
||||
GET /api/open/mailboxes/{id}/messages?folder=Inbox&limit=30&cursor=0&q=keyword
|
||||
GET /api/open/v1/mailboxes/{id}/messages?folder=Inbox&limit=30&cursor=opaque&q=keyword
|
||||
Authorization: Bearer lq_xxx
|
||||
```
|
||||
|
||||
@@ -586,7 +605,7 @@ Query parameters:
|
||||
|
||||
- `folder`: folder name. Defaults to `Inbox`; use `all` for all folders.
|
||||
- `limit`: page size, defaults to `30`, maximum `100`.
|
||||
- `cursor`: numeric offset. Pass back the `nextCursor` value from the previous response to fetch the next page.
|
||||
- `cursor`: opaque stable cursor. Pass back `nextCursor` unchanged. Numeric offsets remain accepted for compatibility.
|
||||
- `q`: optional search keyword. Matches subject, from, to, snippet, and body text.
|
||||
|
||||
<br>
|
||||
@@ -623,8 +642,44 @@ Response:
|
||||
|
||||
`nextCursor` is empty when there are no more pages. Otherwise it contains the offset to pass as `cursor` for the next request.
|
||||
|
||||
当没有更多分页时,`nextCursor` 为空字符串;否则它是下次请求应作为 `cursor` 传入的偏移量。
|
||||
当没有更多分页时,`nextCursor` 为空字符串;否则应将它原样作为下一次请求的 `cursor` 传入。
|
||||
|
||||
Users can only read messages from their own active mailboxes.
|
||||
Users can only read messages from their own active mailboxes. Fetch message bodies and attachment metadata with `GET /api/open/v1/messages/{id}`; download an owned attachment with `GET /api/open/v1/attachments/{id}`.
|
||||
|
||||
用户只能读取自己拥有的 active 邮箱。
|
||||
|
||||
## Additional V1 Endpoints / 其他 V1 接口
|
||||
|
||||
- `GET /api/open/v1/send`: paginated send records.
|
||||
- `GET /api/open/v1/send/{id}/events`: queue audit and final delivery events.
|
||||
- `POST /api/open/v1/send/{id}/retry`: retry a failed queue item.
|
||||
- `POST /api/open/v1/send/{id}/cancel`: cancel a queued or failed item.
|
||||
- `POST /api/open/v1/mailboxes/{id}/password`: reset the owner user's password and all mailbox passwords owned by that user.
|
||||
- `GET /api/open/v1/domains/{id}/dns-records` and `POST .../dns-check`: DNS configuration and check.
|
||||
- `/api/open/v1/aliases`: alias CRUD.
|
||||
|
||||
Domain names and mailbox addresses are immutable. Renaming them requires a storage/identity migration and is intentionally not exposed as a normal update operation.
|
||||
|
||||
域名名称和邮箱地址不可直接修改。重命名需要迁移存储路径及身份信息,因此不作为普通更新操作开放。
|
||||
|
||||
## Delivery Event Webhook / 投递事件回调
|
||||
|
||||
Configure `LANQIN_DELIVERY_WEBHOOK_SECRET`, then post up to 100 events to `POST /api/open/v1/delivery-events`. This endpoint does not accept an API Token. Set the Unix timestamp in `X-LanQin-Timestamp`, compute `HMAC-SHA256(secret, timestamp + "." + rawBody)`, and send the lowercase hexadecimal digest as `X-LanQin-Signature: sha256=<digest>`. Timestamps outside five minutes are rejected. `(provider, event id)` is idempotent.
|
||||
|
||||
配置 `LANQIN_DELIVERY_WEBHOOK_SECRET` 后,可向 `POST /api/open/v1/delivery-events` 一次提交最多 100 条事件。该接口不接受 API Token。将 Unix 时间戳放入 `X-LanQin-Timestamp`,计算 `HMAC-SHA256(secret, timestamp + "." + 原始请求体)`,再以 `X-LanQin-Signature: sha256=<小写十六进制>` 发送。超过五分钟的时间戳会被拒绝;`(provider, event id)` 具备幂等性。
|
||||
|
||||
Accepted event statuses: `delivered`, `bounced`, `complained`, `rejected`, `deferred`. Every event must identify an existing send using `queueId`, `messageId`, or `rfcMessageId`, and its recipient must belong to that send.
|
||||
|
||||
## Outbound Status Webhook / 主动状态推送
|
||||
|
||||
Set `LANQIN_STATUS_WEBHOOK_URL` and `LANQIN_STATUS_WEBHOOK_SECRET` to receive status changes proactively. Events are persisted in a SQLite outbox before delivery. Non-2xx responses are retried with backoff up to 10 attempts. Delivered and retry-exhausted records are removed after 30 days.
|
||||
|
||||
设置 `LANQIN_STATUS_WEBHOOK_URL` 和 `LANQIN_STATUS_WEBHOOK_SECRET` 后,可主动接收状态变化。事件会先持久化到 SQLite outbox,非 2xx 响应会按退避策略重试,最多 10 次;已送达和重试耗尽的记录会在 30 天后清理。
|
||||
|
||||
Outbound requests include `X-LanQin-Webhook-Id`, `X-LanQin-Timestamp`, and `X-LanQin-Signature`. Signature calculation is the same HMAC-SHA256 construction used by the inbound delivery-event endpoint: `HMAC(secret, timestamp + "." + rawBody)`. Event types include `send.accepted`, `send.queued`, `send.retry`, `send.delivered` (upstream SMTP accepted), `send.failed`, `send.canceled`, and `delivery.<final-status>`.
|
||||
|
||||
出站请求包含 `X-LanQin-Webhook-Id`、`X-LanQin-Timestamp` 和 `X-LanQin-Signature`。签名算法与入站投递事件相同:`HMAC(secret, timestamp + "." + 原始请求体)`。事件类型包括 `send.accepted`、`send.queued`、`send.retry`、`send.delivered`(上游 SMTP 接受)、`send.failed`、`send.canceled` 和 `delivery.<最终状态>`。
|
||||
|
||||
The target must be a public HTTPS URL by default. Redirects, URL credentials, loopback, private, link-local, and unspecified addresses are rejected. `LANQIN_STATUS_WEBHOOK_ALLOW_PRIVATE_HOSTS=true` relaxes this for explicitly trusted private deployments and also permits HTTP.
|
||||
|
||||
目标地址默认必须是公网 HTTPS。重定向、URL 用户信息、loopback、私网、链路本地和未指定地址都会被拒绝。只有明确可信的私有部署才应设置 `LANQIN_STATUS_WEBHOOK_ALLOW_PRIVATE_HOSTS=true`;开启后也允许 HTTP。
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "LanQin Email Open API",
|
||||
"version": "1.0.0",
|
||||
"description": "Versioned integration API. The unversioned /api/open routes are compatibility aliases for /api/open/v1."
|
||||
},
|
||||
"servers": [{ "url": "/api/open/v1" }],
|
||||
"security": [{ "bearerAuth": [] }],
|
||||
"paths": {
|
||||
"/domains": { "get": { "parameters": [{ "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/Cursor" }], "responses": { "200": { "description": "Paginated domains" } } }, "post": { "responses": { "201": { "description": "Domain created" } } } },
|
||||
"/domains/{id}": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "get": { "responses": { "200": { "description": "Domain" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "responses": { "200": { "description": "Domain updated" } } }, "delete": { "responses": { "200": { "description": "Domain deleted" }, "404": { "$ref": "#/components/responses/NotFound" } } } },
|
||||
"/domains/{id}/dns-records": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "get": { "responses": { "200": { "description": "Required DNS records" }, "404": { "$ref": "#/components/responses/NotFound" } } } },
|
||||
"/domains/{id}/dns-check": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "post": { "responses": { "200": { "description": "DNS check result" } } } },
|
||||
"/mailboxes": { "get": { "parameters": [{ "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/Cursor" }], "responses": { "200": { "description": "Paginated mailboxes" } } }, "post": { "responses": { "201": { "description": "Mailbox created" } } } },
|
||||
"/mailboxes/{id}": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "get": { "responses": { "200": { "description": "Mailbox" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "responses": { "200": { "description": "Mailbox updated" } } }, "delete": { "responses": { "200": { "description": "Mailbox deleted" }, "404": { "$ref": "#/components/responses/NotFound" } } } },
|
||||
"/mailboxes/{id}/password": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "post": { "responses": { "200": { "description": "Owner and mailbox passwords reset" } } } },
|
||||
"/mailboxes/{id}/messages": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }, { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/Cursor" }], "get": { "responses": { "200": { "description": "Paginated messages" } } } },
|
||||
"/messages/{id}": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "get": { "responses": { "200": { "description": "Message detail" }, "404": { "$ref": "#/components/responses/NotFound" } } } },
|
||||
"/attachments/{id}": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "get": { "responses": { "200": { "description": "Attachment bytes" }, "404": { "$ref": "#/components/responses/NotFound" } } } },
|
||||
"/send": {
|
||||
"get": { "parameters": [{ "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/Cursor" }], "responses": { "200": { "description": "Paginated sends" } } },
|
||||
"post": {
|
||||
"parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
|
||||
"requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" } } } },
|
||||
"responses": { "200": { "description": "Idempotent replay", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendStatus" } } } }, "201": { "description": "Queued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendStatus" } } } }, "409": { "$ref": "#/components/responses/Conflict" } }
|
||||
}
|
||||
},
|
||||
"/send/{id}": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "get": { "responses": { "200": { "description": "Send status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendStatus" } } } }, "404": { "$ref": "#/components/responses/NotFound" } } } },
|
||||
"/send/{id}/events": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "get": { "responses": { "200": { "description": "Queue audit and delivery events" }, "404": { "$ref": "#/components/responses/NotFound" } } } },
|
||||
"/send/{id}/retry": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "post": { "responses": { "200": { "description": "Send requeued" }, "409": { "$ref": "#/components/responses/Conflict" } } } },
|
||||
"/send/{id}/cancel": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "post": { "responses": { "200": { "description": "Send canceled" }, "409": { "$ref": "#/components/responses/Conflict" } } } },
|
||||
"/aliases": { "get": { "parameters": [{ "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/Cursor" }], "responses": { "200": { "description": "Paginated aliases" } } }, "post": { "responses": { "201": { "description": "Alias created" } } } },
|
||||
"/aliases/{id}": { "parameters": [{ "$ref": "#/components/parameters/ResourceId" }], "get": { "responses": { "200": { "description": "Alias" }, "404": { "$ref": "#/components/responses/NotFound" } } }, "post": { "responses": { "200": { "description": "Alias updated" } } }, "delete": { "responses": { "200": { "description": "Alias deleted" }, "404": { "$ref": "#/components/responses/NotFound" } } } },
|
||||
"/delivery-events": {
|
||||
"post": {
|
||||
"security": [],
|
||||
"description": "HMAC-SHA256 signed delivery event callback. Sign timestamp + '.' + raw body.",
|
||||
"parameters": [
|
||||
{ "name": "X-LanQin-Timestamp", "in": "header", "required": true, "schema": { "type": "string" } },
|
||||
{ "name": "X-LanQin-Signature", "in": "header", "required": true, "schema": { "type": "string", "pattern": "^sha256=[a-f0-9]{64}$" } }
|
||||
],
|
||||
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["events"], "properties": { "events": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "$ref": "#/components/schemas/DeliveryEventInput" } } } } } } },
|
||||
"responses": { "200": { "description": "Events stored or deduplicated" }, "401": { "$ref": "#/components/responses/Unauthorized" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": { "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "lq_* API Token" } },
|
||||
"parameters": {
|
||||
"IdempotencyKey": { "name": "Idempotency-Key", "in": "header", "required": false, "description": "Up to 128 characters; retained for 24 hours.", "schema": { "type": "string", "maxLength": 128 } },
|
||||
"ResourceId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
|
||||
"Limit": { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100 } },
|
||||
"Cursor": { "name": "cursor", "in": "query", "required": false, "schema": { "type": "string" } }
|
||||
},
|
||||
"responses": {
|
||||
"Unauthorized": { "description": "Missing or invalid authentication" },
|
||||
"NotFound": { "description": "Resource not found or not owned by the token user" },
|
||||
"Conflict": { "description": "Idempotency key conflict or concurrent state change" }
|
||||
},
|
||||
"schemas": {
|
||||
"SendRequest": {
|
||||
"type": "object", "required": ["mailboxId"],
|
||||
"properties": {
|
||||
"mailboxId": { "type": "string" }, "from": { "type": "string" }, "fromName": { "type": "string" },
|
||||
"to": { "type": "array", "items": { "type": "string", "format": "email" } },
|
||||
"cc": { "type": "array", "items": { "type": "string", "format": "email" } },
|
||||
"bcc": { "type": "array", "items": { "type": "string", "format": "email" } },
|
||||
"subject": { "type": "string" }, "text": { "type": "string" }, "html": { "type": "string" },
|
||||
"attachments": { "type": "array", "items": { "$ref": "#/components/schemas/AttachmentInput" } }
|
||||
}
|
||||
},
|
||||
"AttachmentInput": { "type": "object", "required": ["filename", "contentBase64"], "properties": { "filename": { "type": "string" }, "contentType": { "type": "string" }, "contentBase64": { "type": "string", "contentEncoding": "base64" } } },
|
||||
"SendStatus": {
|
||||
"type": "object", "required": ["id", "status", "messageId", "rfcMessageId", "mailboxId", "createdAt"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "description": "Stable sent message id (mail_*)." },
|
||||
"queueId": { "type": "string", "description": "Internal queue id (snd_*)." },
|
||||
"status": { "type": "string", "enum": ["accepted", "queued", "sending", "relayed", "failed", "canceled", "delivered", "bounced", "complained", "rejected", "deferred", "partial"] },
|
||||
"queueStatus": { "type": "string", "enum": ["queued", "sending", "delivered", "failed", "canceled"] },
|
||||
"messageId": { "type": "string" }, "rfcMessageId": { "type": "string" }, "mailboxId": { "type": "string" },
|
||||
"mailboxAddress": { "type": "string" }, "subject": { "type": "string" }, "recipients": { "type": "array", "items": { "type": "string" } },
|
||||
"recipientStatuses": { "type": "array", "items": { "$ref": "#/components/schemas/RecipientStatus" } },
|
||||
"createdAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"RecipientStatus": { "type": "object", "required": ["recipient", "status", "occurredAt"], "properties": { "recipient": { "type": "string" }, "status": { "type": "string" }, "reason": { "type": "string" }, "provider": { "type": "string" }, "occurredAt": { "type": "string", "format": "date-time" } } },
|
||||
"DeliveryEventInput": {
|
||||
"type": "object", "required": ["id", "provider", "recipient", "status", "occurredAt"],
|
||||
"properties": {
|
||||
"id": { "type": "string" }, "provider": { "type": "string" }, "queueId": { "type": "string" }, "messageId": { "type": "string" }, "rfcMessageId": { "type": "string" },
|
||||
"recipient": { "type": "string", "format": "email" }, "status": { "type": "string", "enum": ["delivered", "bounced", "complained", "rejected", "deferred"] },
|
||||
"reason": { "type": "string" }, "occurredAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
},
|
||||
"Error": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string" } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user