type: 收敛测试 schemas 与协议层类型

This commit is contained in:
yml2213
2026-08-30 20:35:08 +08:00
parent 92dc461e52
commit c891ac982e
26 changed files with 1846 additions and 882 deletions
+70 -37
View File
@@ -49,7 +49,9 @@ class EmailVerifier:
self.password = password # 邮箱密码
self.timeout = timeout
self.roundcube_url = roundcube_url or ROUNDCUBE_URL
self.backup_passwords = backup_passwords if backup_passwords is not None else EMAIL_BACKUP_PASSWORDS
self.backup_passwords = (
backup_passwords if backup_passwords is not None else EMAIL_BACKUP_PASSWORDS
)
# Roundcube 会话(懒初始化)
self._rc_session: Optional[requests.Session] = None
@@ -87,7 +89,9 @@ class EmailVerifier:
for index, password in enumerate(passwords):
is_backup = index > 0
ok, password_failed = self._try_roundcube_login(password, is_backup=is_backup)
ok, password_failed = self._try_roundcube_login(
password, is_backup=is_backup
)
if ok:
return True
credential_failed = credential_failed or password_failed
@@ -112,13 +116,17 @@ class EmailVerifier:
seen.add(password)
return result
def _try_roundcube_login(self, password: str, is_backup: bool = False) -> tuple[bool, bool]:
def _try_roundcube_login(
self, password: str, is_backup: bool = False
) -> tuple[bool, bool]:
"""尝试一次 Roundcube 登录,返回 (是否成功, 是否明确为密码错误)。"""
try:
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
})
session.headers.update(
{
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
}
)
# 1. 访问首页获取 token
resp = session.get(self.roundcube_url, timeout=self.timeout)
@@ -143,17 +151,20 @@ class EmailVerifier:
)
# 3. 检查登录是否成功:登录失败页也可能有 request_token,不能只靠 token 判定。
new_token_match = re.search(r'request_token["\s:]+["\']([^"\']+)', resp.text)
new_token_match = re.search(
r'request_token["\s:]+["\']([^"\']+)', resp.text
)
if not new_token_match:
logger.warning("Roundcube: 登录失败(未找到 request_token")
return False, False
text_lower = resp.text.lower()
login_failed = "_err=loginfailed" in resp.url or "loginfailed" in text_lower
login_form_present = 'name="_user"' in resp.text and 'name="_pass"' in resp.text
mail_page = (
"_task=mail" in resp.url
or re.search(r'(?:env\.task\s*=\s*|["\']task["\']\s*:\s*)["\']mail["\']', resp.text)
login_form_present = (
'name="_user"' in resp.text and 'name="_pass"' in resp.text
)
mail_page = "_task=mail" in resp.url or re.search(
r'(?:env\.task\s*=\s*|["\']task["\']\s*:\s*)["\']mail["\']', resp.text
)
if login_failed or login_form_present or not mail_page:
@@ -183,6 +194,8 @@ class EmailVerifier:
if self._rc_login_error:
raise EmailLoginError(self._rc_login_error)
return []
if self._rc_session is None:
return []
try:
resp = self._rc_session.get(
@@ -201,7 +214,7 @@ class EmailVerifier:
# 匹配: this.add_message_row(UID, {subject:"...", fromto:"...", date:"今天 09:02", size:"5 KB"}, {...}, ...);
for m in re.finditer(
r'add_message_row\((\d+),\s*(\{[^}]+\})',
r"add_message_row\((\d+),\s*(\{[^}]+\})",
exec_text,
):
uid = int(m.group(1))
@@ -210,11 +223,13 @@ class EmailVerifier:
subject_m = re.search(r'"subject"\s*:\s*"([^"]*)"', props_str)
date_m = re.search(r'"date"\s*:\s*"([^"]*)"', props_str)
messages.append({
"uid": uid,
"subject": subject_m.group(1) if subject_m else "",
"date": date_m.group(1) if date_m else "",
})
messages.append(
{
"uid": uid,
"subject": subject_m.group(1) if subject_m else "",
"date": date_m.group(1) if date_m else "",
}
)
return messages
@@ -230,11 +245,12 @@ class EmailVerifier:
if self._rc_login_error:
raise EmailLoginError(self._rc_login_error)
return None
if self._rc_session is None:
return None
try:
resp = self._rc_session.get(
f"{self.roundcube_url}?_task=mail&_action=show"
f"&_mbox=INBOX&_uid={uid}",
f"{self.roundcube_url}?_task=mail&_action=show&_mbox=INBOX&_uid={uid}",
timeout=self.timeout,
)
return resp.text
@@ -280,8 +296,14 @@ class EmailVerifier:
# 星期X
weekday_map = {
"星期一": 0, "星期二": 1, "星期三": 2, "星期四": 3,
"星期": 4, "星期六": 5, "星期日": 6, "星期天": 6,
"星期一": 0,
"星期": 1,
"星期三": 2,
"星期四": 3,
"星期五": 4,
"星期六": 5,
"星期日": 6,
"星期天": 6,
}
for prefix, wd in weekday_map.items():
if date_str.startswith(prefix):
@@ -295,7 +317,9 @@ class EmailVerifier:
target = now
else:
target = now - timedelta(days=days_ago)
return target.replace(hour=int(h), minute=int(m), second=0, microsecond=0)
return target.replace(
hour=int(h), minute=int(m), second=0, microsecond=0
)
except (ValueError, AttributeError):
return None
@@ -366,7 +390,9 @@ class EmailVerifier:
raise InterruptedError("任务已停止")
try:
code = self._fetch_code_via_roundcube(after_timestamp, allow_old_seconds)
code = self._fetch_code_via_roundcube(
after_timestamp, allow_old_seconds
)
if code:
logger.success(f"获取到验证码: {code}")
return code
@@ -383,7 +409,9 @@ class EmailVerifier:
raise InterruptedError("任务已停止")
time.sleep(min(0.2, sleep_deadline - time.monotonic()))
raise TimeoutError(f"等待验证码超时{'' + last_error + '' if last_error else ''}")
raise TimeoutError(
f"等待验证码超时{'' + last_error + '' if last_error else ''}"
)
# ── Roundcube 方式获取验证码 ────────────────────────────
@@ -397,7 +425,8 @@ class EmailVerifier:
# 筛选斗鱼验证码邮件(按 UID 降序,即最新的先看)
douyu_msgs = [
msg for msg in messages
msg
for msg in messages
if "斗鱼" in msg.get("subject", "") or "验证码" in msg.get("subject", "")
]
@@ -407,7 +436,9 @@ class EmailVerifier:
msg_dt = self._parse_rc_date(msg["date"])
if msg_dt is None:
# 日期无法解析的旧邮件(如完整时间戳),在有 after_timestamp 时跳过
logger.debug(f"Roundcube: 邮件 UID={msg['uid']} 日期 '{msg['date']}' 无法解析,已跳过")
logger.debug(
f"Roundcube: 邮件 UID={msg['uid']} 日期 '{msg['date']}' 无法解析,已跳过"
)
continue
# Roundcube 日期只有分钟精度(如"今天 14:20"→14:20:00),
# 而 after_timestamp 是秒级精度(如14:20:49)。
@@ -415,7 +446,9 @@ class EmailVerifier:
# 解决:将 after_timestamp 也向下取整到分钟后再比较。
after_minute_ts = after_timestamp - (after_timestamp % 60)
if msg_dt.timestamp() < after_minute_ts - allow_old_seconds:
logger.debug(f"Roundcube: 邮件 UID={msg['uid']} 日期 {msg['date']} 早于发送时间,跳过")
logger.debug(
f"Roundcube: 邮件 UID={msg['uid']} 日期 {msg['date']} 早于发送时间,跳过"
)
continue
# 读取邮件正文提取验证码
@@ -436,16 +469,16 @@ class EmailVerifier:
"""从文本中提取6位验证码"""
# 清理HTML标签和实体
text = html.unescape(text)
text = re.sub(r'<[^>]+>', ' ', text)
text = re.sub(r'\s+', ' ', text)
text = re.sub(r"<[^>]+>", " ", text)
text = re.sub(r"\s+", " ", text)
# 查找6位数字验证码(优先匹配带上下文的)
patterns = [
r'(?:验证码|校验码|动态码|安全码)\s*(?:是|为|:|)?\s*(\d{6})',
r'(?:您的|你的|本次)?(?:验证码|校验码|动态码|安全码)\s*(?:是|为)?\s*(\d{6})',
r'(\d{6})\s*(?:是|为)?(?:您的|你的|本次)?(?:验证码|校验码|动态码|安全码)',
r'(?:verification\s+code|security\s+code|code)\s*(?:is|:|)?\s*(\d{6})',
r'\b(\d{6})\b',
r"(?:验证码|校验码|动态码|安全码)\s*(?:是|为|:|)?\s*(\d{6})",
r"(?:您的|你的|本次)?(?:验证码|校验码|动态码|安全码)\s*(?:是|为)?\s*(\d{6})",
r"(\d{6})\s*(?:是|为)?(?:您的|你的|本次)?(?:验证码|校验码|动态码|安全码)",
r"(?:verification\s+code|security\s+code|code)\s*(?:is|:|)?\s*(\d{6})",
r"\b(\d{6})\b",
]
for pattern in patterns:
@@ -459,7 +492,7 @@ class EmailVerifier:
def get_email_config_for_account(email_address: str) -> dict:
"""根据邮箱地址返回配置(HTTP 模式下仅用于兼容)"""
return {
'server': '',
'port': 143,
'ssl': False,
"server": "",
"port": 143,
"ssl": False,
}