Spaces:
Paused
Paused
File size: 6,551 Bytes
19162c0 70a6b78 19162c0 70a6b78 19162c0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | """Alias + Reader 配对 MailProvider — 兼容 alias forwarding 注册场景。
Addy.io / SimpleLogin 这类 alias forwarding 服务的 alias 域不在 OpenAI 公开 disposable
黑名单,适合用作注册时的"未黑域名"邮箱来源,但**没有内置 inbox** —— 收到的邮件会被
转发到底层真实邮箱(用户的 ProtonMail / 自托管 Postfix / maillab / cf 等)。
`AliasWithReaderProvider` 把两个 provider 组合成一个 `MailProvider`,对外暴露统一接口:
- 写入路径(`create_temp_email` / `delete_account` / `list_accounts`)走 **alias**:
拿到不在黑名单的临时邮箱
- 读取路径(`search_emails_by_recipient` / `list_emails` / `delete_emails_for` /
`wait_for_email` / `get_latest_emails`)走 **reader**:在底层真实邮箱里查 OpenAI
转发过来的 OTP / 邀请邮件
⚠️ 关键假设(运维侧配置,不在代码内):
用户已在 Addy.io / SimpleLogin 控制台把 alias 的转发目标设为 reader 关联的真实邮箱
(例如 maillab 创建的子邮箱地址)。本 provider 不负责验证转发链路,只负责调用对接。
详见 S4 PRD §Q3 与 S2 PRD §Q2(alias forwarding 留白)。
"""
from __future__ import annotations
import logging
from typing import Any
from autoteam.mail.base import MailProvider
logger = logging.getLogger(__name__)
class AliasWithReaderProvider(MailProvider):
"""组合一个 alias provider(写)+ 一个 reader provider(读)。
Args:
alias: 实现 `create_temp_email` / `delete_account` / `list_accounts`
能拿到不在 OpenAI 黑名单的邮箱地址(如 Addy.io / SimpleLogin)。
reader: 实现 `search_emails_by_recipient` / `list_emails` / `delete_emails_for`
能读到 alias 转发过来的邮件(如 maillab / cf_temp_email / IMAP 自建)。
Usage:
pair = AliasWithReaderProvider(alias=AddyIoClient(), reader=MaillabClient())
acc_id, email = pair.create_temp_email(prefix="autoteam")
# OpenAI 注册 → OTP 邮件被转发到 reader 的真实邮箱
otp_email = pair.wait_for_email(email, timeout=180, sender_keyword="openai")
"""
provider_name = "alias_with_reader"
def __init__(self, alias: MailProvider, reader: MailProvider):
if alias is None or reader is None:
raise ValueError("AliasWithReaderProvider 需要同时提供 alias 与 reader 两个 provider")
self.alias: MailProvider = alias
self.reader: MailProvider = reader
# 复合名:便于日志 / cache_key 区分
self.provider_name = (
f"alias_with_reader[{getattr(alias, 'provider_name', 'alias')}+"
f"{getattr(reader, 'provider_name', 'reader')}]"
)
# ------------------------------------------------------------------ auth
def login(self) -> str:
"""同时初始化 alias 与 reader,任一抛异常视为整体不可用。
返回值仅用于"是否成功登录"的简单语义,**不**包含任何真实 token —
历史版本曾把两个 provider 的 token 用 ``|`` 拼接返回,有调用方误存到
accounts.json / log 的风险(round-12 wire-up audit minor m1)。
现在改为返回固定 ``"ok"`` 字面量,token 仅在 provider 实例内部保持。
"""
self.alias.login()
self.reader.login()
logger.info(
"[alias_with_reader] login OK (alias=%s, reader=%s)",
getattr(self.alias, "provider_name", "alias"),
getattr(self.reader, "provider_name", "reader"),
)
return "ok"
# ------------------------------------------------------------------ write → alias
def create_temp_email(
self, prefix: str | None = None, domain: str | None = None
) -> tuple[int | str, str]:
return self.alias.create_temp_email(prefix=prefix, domain=domain)
def delete_account(self, account_id: int | str) -> dict:
return self.alias.delete_account(account_id)
def list_accounts(self, size: int = 200) -> list[dict]:
return self.alias.list_accounts(size=size)
# ------------------------------------------------------------------ read → reader
def search_emails_by_recipient(
self, to_email: str, size: int = 10, account_id: int | str | None = None
) -> list[dict]:
return self.reader.search_emails_by_recipient(
to_email, size=size, account_id=account_id
)
def list_emails(self, account_id: int | str, size: int = 10) -> list[dict]:
return self.reader.list_emails(account_id, size=size)
def delete_emails_for(self, to_email: str) -> int:
return self.reader.delete_emails_for(to_email)
def get_latest_emails(
self, account_id: int | str, email_id: int = 0, all_receive: int = 0
) -> list[dict]:
return self.reader.get_latest_emails(account_id, email_id=email_id, all_receive=all_receive)
# ------------------------------------------------------------------ shared (text 工具) — 用 reader 的实现
def extract_verification_code(self, email_data: dict) -> str | None:
return self.reader.extract_verification_code(email_data)
def extract_invite_link(self, email_data: dict) -> str | None:
return self.reader.extract_invite_link(email_data)
def wait_for_email(
self,
to_email: str,
timeout: int | None = None,
sender_keyword: str | None = None,
) -> dict:
# 显式委派 reader,避免 base.wait_for_email 默认实现回头调本类的
# search_emails_by_recipient(本类已委派到 reader,效果一致,但语义更明确)。
return self.reader.wait_for_email(to_email, timeout=timeout, sender_keyword=sender_keyword)
# ------------------------------------------------------------------ introspection
def __repr__(self) -> str: # pragma: no cover - 调试辅助
return (
f"AliasWithReaderProvider("
f"alias={getattr(self.alias, 'provider_name', type(self.alias).__name__)}, "
f"reader={getattr(self.reader, 'provider_name', type(self.reader).__name__)})"
)
def describe(self) -> dict[str, Any]:
"""供日志 / UI 展示的结构化描述。"""
return {
"type": "alias_with_reader",
"alias": getattr(self.alias, "provider_name", type(self.alias).__name__),
"reader": getattr(self.reader, "provider_name", type(self.reader).__name__),
}
__all__ = ["AliasWithReaderProvider"]
|