type: 收敛测试 schemas 与协议层类型
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user