Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions outlook_web/controllers/external_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,7 @@ def _check_pool_access(endpoint: str):
)


@api_key_required
@external_api_guards(feature="pool_claim_random")
def api_external_pool_claim_random():
endpoint = "/api/external/pool/claim-random"
def _claim_pool_account(endpoint: str, *, require_email_domain: bool = False):
disabled_resp = _check_pool_external_enabled(endpoint)
if disabled_resp is not None:
return disabled_resp
Expand All @@ -99,6 +96,10 @@ def api_external_pool_claim_random():
project_key = body.get("project_key")
email_domain = body.get("email_domain")

if require_email_domain and not str(email_domain or "").strip():
_audit(endpoint, "error", details={"code": "EMAIL_DOMAIN_REQUIRED"})
return jsonify(external_api_service.fail("EMAIL_DOMAIN_REQUIRED", "email_domain 不能为空")), 400

try:
account = claim_random(
caller_id=caller_id,
Expand Down Expand Up @@ -137,6 +138,18 @@ def api_external_pool_claim_random():
return jsonify(external_api_service.fail("INTERNAL_ERROR", "服务内部错误")), 500


@api_key_required
@external_api_guards(feature="pool_claim_random")
def api_external_pool_claim_random():
return _claim_pool_account("/api/external/pool/claim-random")


@api_key_required
@external_api_guards(feature="pool_claim_random")
def api_external_pool_claim_domain():
return _claim_pool_account("/api/external/pool/claim-domain", require_email_domain=True)


@api_key_required
@external_api_guards(feature="pool_claim_release")
def api_external_pool_claim_release():
Expand Down
5 changes: 5 additions & 0 deletions outlook_web/routes/external_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ def create_blueprint(csrf_exempt: Optional[Callable] = None) -> Blueprint:
external_pool_controller.api_external_pool_claim_random,
["POST"],
),
(
"/api/external/pool/claim-domain",
external_pool_controller.api_external_pool_claim_domain,
["POST"],
),
(
"/api/external/pool/claim-release",
external_pool_controller.api_external_pool_claim_release,
Expand Down
41 changes: 37 additions & 4 deletions registration-mail-pool-api.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ Time fields use ISO 8601, for example:

| Endpoint | Purpose | Recommended |
| --- | --- | --- |
| `POST /api/external/pool/claim-random` | claim a mailbox | Common |
| `POST /api/external/pool/claim-random` | claim a mailbox, optionally filtered by domain | Common |
| `POST /api/external/pool/claim-domain` | claim a mailbox from a required domain | Common |
| `POST /api/external/pool/claim-release` | release a mailbox | Common |
| `POST /api/external/pool/claim-complete` | submit the task result | Common |
| `GET /api/external/pool/stats` | inspect pool counts | Optional |
Expand Down Expand Up @@ -324,12 +325,15 @@ Request body:
| `caller_id` | string | Yes | caller instance, node, or worker identity |
| `task_id` | string | Yes | unique task ID |
| `provider` | string | No | provider filter: `outlook` / `imap` / `custom` / `cloudflare_temp_mail` |
| `project_key` | string | No | project-level reuse and duplicate-prevention context |
| `email_domain` | string | No | mailbox domain filter; when provided, only eligible mailboxes in that domain are claimed |

Current implementation notes:

- the current pool API supports filtering only by `provider`
- `outlook.com`, `hotmail.com`, `live.com`, and `live.cn` all map to `provider=outlook`
- the current external pool API does not support extra filtering by domain, group, or tags
- `claim-random` claims an eligible mailbox randomly by default; when `email_domain` is provided, it claims randomly within that domain
- for a clearer domain-specific contract, use `POST /api/external/pool/claim-domain`; that endpoint requires `email_domain`
- `outlook.com`, `hotmail.com`, `live.com`, and `live.cn` all map to `provider=outlook`; use `email_domain` when those domains need to be distinguished
- the current external pool API does not support claiming a specific full mailbox, group, or tag
- when `provider=cloudflare_temp_mail` and no eligible mailbox exists in pool, the service dynamically creates a CF temp mailbox and returns it as claimed

Success response fields:
Expand Down Expand Up @@ -391,6 +395,35 @@ No-available response example:
}
```

### `POST /api/external/pool/claim-domain`

Purpose: claim a mailbox from a specific domain. This is the explicit endpoint for `claim-random + email_domain`; internally it reuses the same pool claim, lease, audit, and completion state machine.

Request body:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `caller_id` | string | Yes | caller instance, node, or worker identity |
| `task_id` | string | Yes | unique task ID |
| `email_domain` | string | Yes | mailbox domain to claim from, for example `zerodotsix.top` |
| `provider` | string | No | optional provider filter |
| `project_key` | string | No | project-level reuse and duplicate-prevention context |

If `email_domain` is missing or blank, the endpoint returns HTTP `400` with code `EMAIL_DOMAIN_REQUIRED`.

Copy-paste example:

```bash
curl -X POST https://api.example.com/api/external/pool/claim-domain \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"caller_id": "reg-worker-001",
"task_id": "task-20260409-0001",
"email_domain": "zerodotsix.top"
}'
```

### `POST /api/external/pool/claim-release`

Request body:
Expand Down
1 change: 1 addition & 0 deletions tests/test_external_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ def fake_csrf_exempt(handler):
set(wrapped_handlers),
{
"api_external_pool_claim_random",
"api_external_pool_claim_domain",
"api_external_pool_claim_release",
"api_external_pool_claim_complete",
"api_external_pool_stats",
Expand Down
33 changes: 33 additions & 0 deletions tests/test_pool_flow_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,39 @@ def test_claim_response_includes_email_domain_and_claimed_at(self):
self.assertIn("claimed_at", data["data"])
self.assertIsNotNone(data["data"]["claimed_at"])

def test_claim_domain_requires_email_domain(self):
resp = self.client.post(
"/api/external/pool/claim-domain",
headers=self._auth_headers(),
json={"caller_id": "domain_bot", "task_id": "domain_required"},
)
self.assertEqual(resp.status_code, 400)
data = json.loads(resp.data)
self.assertFalse(data["success"])
self.assertEqual(data["code"], "EMAIL_DOMAIN_REQUIRED")

def test_claim_domain_filters_to_requested_domain(self):
requested_domain = f"requested_{uuid.uuid4().hex[:8]}.test"
other_domain = f"other_{uuid.uuid4().hex[:8]}.test"
requested = self._make_pool_account(email_domain=requested_domain)
other = self._make_pool_account(email_domain=other_domain)

resp = self.client.post(
"/api/external/pool/claim-domain",
headers=self._auth_headers(),
json={
"caller_id": "domain_bot",
"task_id": "domain_claim",
"email_domain": requested_domain.upper(),
},
)
self.assertEqual(resp.status_code, 200)
data = json.loads(resp.data)
self.assertTrue(data["success"])
self.assertEqual(data["data"]["account_id"], requested["id"])
self.assertEqual(data["data"]["email_domain"], requested_domain)
self.assertNotEqual(data["data"]["account_id"], other["id"])

def test_claim_with_project_key_prevents_same_project_reuse_without_manual_status_reset(self):
"""同 caller_id + project_key 下,新语义应原生阻止再次领取,无需手工改状态。"""
# 使用唯一的 email_domain 隔离测试数据
Expand Down
41 changes: 37 additions & 4 deletions 注册与邮箱池接口文档.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ X-API-Key: YOUR_API_KEY

| 接口 | 说明 | 是否推荐 |
| --- | --- | --- |
| `POST /api/external/pool/claim-random` | 领取邮箱 | 常用 |
| `POST /api/external/pool/claim-random` | 随机领取邮箱,可选按域名过滤 | 常用 |
| `POST /api/external/pool/claim-domain` | 指定域名领取邮箱 | 常用 |
| `POST /api/external/pool/claim-release` | 释放邮箱 | 常用 |
| `POST /api/external/pool/claim-complete` | 回传任务结果 | 常用 |
| `GET /api/external/pool/stats` | 查看池状态 | 可选 |
Expand Down Expand Up @@ -323,12 +324,15 @@ curl -X GET https://api.example.com/api/external/health \
| `caller_id` | string | 是 | 调用方实例、节点或 worker 标识 |
| `task_id` | string | 是 | 当前任务唯一 ID |
| `provider` | string | 否 | 提供商筛选:`outlook` / `imap` / `custom` / `cloudflare_temp_mail` |
| `project_key` | string | 否 | 项目维度复用与防重复上下文 |
| `email_domain` | string | 否 | 邮箱域名筛选;传入后只在该域名下领取可用邮箱 |

当前实现说明:

- 当前池接口只支持按 `provider` 筛选
- `outlook.com`、`hotmail.com`、`live.com`、`live.cn` 当前都归属于 `provider=outlook`
- 当前对外池接口不支持按域名、分组、标签进一步筛选
- `claim-random` 默认随机领取可用邮箱;传入 `email_domain` 时,会在该域名范围内随机领取
- 如需语义更明确的指定域名领取,可使用 `POST /api/external/pool/claim-domain`,该接口要求 `email_domain` 必填
- `outlook.com`、`hotmail.com`、`live.com`、`live.cn` 当前都归属于 `provider=outlook`,如需区分这些域名,请使用 `email_domain`
- 当前对外池接口不支持指定完整邮箱、分组或标签领取
- 当 `provider=cloudflare_temp_mail` 且池中无可用邮箱时,服务会动态创建 CF 临时邮箱并直接返回领取结果

成功返回字段:
Expand Down Expand Up @@ -390,6 +394,35 @@ curl -X POST https://api.example.com/api/external/pool/claim-random \
}
```

### `POST /api/external/pool/claim-domain`

用途:指定域名领取邮箱。该接口是 `claim-random + email_domain` 的显式入口,内部复用同一套邮箱池领取、租约、审计和回传状态机。

请求体参数:

| 参数名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `caller_id` | string | 是 | 调用方实例、节点或 worker 标识 |
| `task_id` | string | 是 | 当前任务唯一 ID |
| `email_domain` | string | 是 | 指定领取的邮箱域名,例如 `zerodotsix.top` |
| `provider` | string | 否 | 可选提供商筛选 |
| `project_key` | string | 否 | 项目维度复用与防重复上下文 |

如果未传入 `email_domain` 或传入空字符串,返回 HTTP `400`,错误码为 `EMAIL_DOMAIN_REQUIRED`。

可复制示例:

```bash
curl -X POST https://api.example.com/api/external/pool/claim-domain \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"caller_id": "reg-worker-001",
"task_id": "task-20260409-0001",
"email_domain": "zerodotsix.top"
}'
```

### `POST /api/external/pool/claim-release`

请求体参数:
Expand Down
Loading