优化邮箱登录错误分支,增加备用密码

This commit is contained in:
yml2213
2026-06-25 09:58:46 +08:00
parent e94b37c602
commit f736ef4f4c
2 changed files with 76 additions and 8 deletions
+72 -7
View File
@@ -14,6 +14,15 @@ import requests
# Roundcube Webmail 地址。可通过环境变量覆盖。空值时回退到默认。 # Roundcube Webmail 地址。可通过环境变量覆盖。空值时回退到默认。
ROUNDCUBE_URL = os.getenv("MAIL_ROUNDCUBE_URL") or "http://111.229.206.54:8000/" ROUNDCUBE_URL = os.getenv("MAIL_ROUNDCUBE_URL") or "http://111.229.206.54:8000/"
EMAIL_BACKUP_PASSWORDS = [
pwd.strip()
for pwd in os.getenv("MAIL_BACKUP_PASSWORDS", "aa778899").split(";")
if pwd.strip()
]
class EmailLoginError(RuntimeError):
"""邮箱登录失败,通常是邮箱账号或密码错误。"""
class EmailVerifier: class EmailVerifier:
@@ -31,6 +40,7 @@ class EmailVerifier:
max_messages: int = 20, max_messages: int = 20,
use_ssl: bool = False, use_ssl: bool = False,
roundcube_url: str = "", roundcube_url: str = "",
backup_passwords: Optional[list[str]] = None,
): ):
# 兼容旧参数名 # 兼容旧参数名
self.imap_server = imap_server self.imap_server = imap_server
@@ -39,11 +49,13 @@ class EmailVerifier:
self.password = password # 邮箱密码 self.password = password # 邮箱密码
self.timeout = timeout self.timeout = timeout
self.roundcube_url = roundcube_url or ROUNDCUBE_URL self.roundcube_url = roundcube_url or ROUNDCUBE_URL
self.backup_passwords = backup_passwords if backup_passwords is not None else EMAIL_BACKUP_PASSWORDS
# Roundcube 会话(懒初始化) # Roundcube 会话(懒初始化)
self._rc_session: Optional[requests.Session] = None self._rc_session: Optional[requests.Session] = None
self._rc_token: Optional[str] = None self._rc_token: Optional[str] = None
self._rc_logged_in = False self._rc_logged_in = False
self._rc_login_error = ""
def connect(self) -> None: def connect(self) -> None:
"""兼容接口""" """兼容接口"""
@@ -69,6 +81,39 @@ class EmailVerifier:
if self._rc_logged_in and self._rc_session and self._rc_token: if self._rc_logged_in and self._rc_session and self._rc_token:
return True return True
self._rc_login_error = ""
credential_failed = False
passwords = self._candidate_passwords()
for index, password in enumerate(passwords):
is_backup = index > 0
ok, password_failed = self._try_roundcube_login(password, is_backup=is_backup)
if ok:
return True
credential_failed = credential_failed or password_failed
if password_failed and index == 0 and len(passwords) > 1:
logger.info("Roundcube: 主邮箱密码失败,尝试备用邮箱密码")
if credential_failed:
self._rc_login_error = "邮箱登录失败"
logger.warning(f"Roundcube: {self._rc_login_error}")
return False
def _candidate_passwords(self) -> list[str]:
"""返回去重后的邮箱密码候选列表:主密码优先,其次备用密码。"""
candidates = [self.password, *self.backup_passwords]
result = []
seen = set()
for password in candidates:
if not password or password in seen:
continue
result.append(password)
seen.add(password)
return result
def _try_roundcube_login(self, password: str, is_backup: bool = False) -> tuple[bool, bool]:
"""尝试一次 Roundcube 登录,返回 (是否成功, 是否明确为密码错误)。"""
try: try:
session = requests.Session() session = requests.Session()
session.headers.update({ session.headers.update({
@@ -80,7 +125,7 @@ class EmailVerifier:
token_match = re.search(r'name="_token"\s+value="([^"]+)"', resp.text) token_match = re.search(r'name="_token"\s+value="([^"]+)"', resp.text)
if not token_match: if not token_match:
logger.warning("Roundcube: 未获取到登录 token") logger.warning("Roundcube: 未获取到登录 token")
return False return False, False
token = token_match.group(1) token = token_match.group(1)
# 2. 登录 # 2. 登录
@@ -91,27 +136,41 @@ class EmailVerifier:
"_task": "login", "_task": "login",
"_action": "login", "_action": "login",
"_user": self.username, "_user": self.username,
"_pass": self.password, "_pass": password,
}, },
timeout=self.timeout, timeout=self.timeout,
allow_redirects=True, allow_redirects=True,
) )
# 3. 检查登录是否成功(成功会跳到 ?_task=mail # 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: if not new_token_match:
logger.warning("Roundcube: 登录失败(未找到 request_token") logger.warning("Roundcube: 登录失败(未找到 request_token")
return False 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)
)
if login_failed or login_form_present or not mail_page:
password_name = "备用邮箱密码" if is_backup else "主邮箱密码"
logger.warning(f"Roundcube: {password_name}登录失败")
return False, True
self._rc_session = session self._rc_session = session
self._rc_token = new_token_match.group(1) self._rc_token = new_token_match.group(1)
self._rc_logged_in = True self._rc_logged_in = True
logger.debug(f"Roundcube: 登录成功 ({self.username})") password_name = "备用邮箱密码" if is_backup else "主邮箱密码"
return True logger.debug(f"Roundcube: {password_name}登录成功 ({self.username})")
return True, False
except Exception as e: except Exception as e:
logger.warning(f"Roundcube: 登录异常: {e}") logger.warning(f"Roundcube: 登录异常: {e}")
return False return False, False
# ── Roundcube 邮件列表(含精确日期) ──────────────────── # ── Roundcube 邮件列表(含精确日期) ────────────────────
@@ -121,6 +180,8 @@ class EmailVerifier:
返回 [{"uid": 2, "subject": "验证码 - 斗鱼", "date": "今天 09:02"}, ...] 返回 [{"uid": 2, "subject": "验证码 - 斗鱼", "date": "今天 09:02"}, ...]
""" """
if not self._ensure_roundcube_session(): if not self._ensure_roundcube_session():
if self._rc_login_error:
raise EmailLoginError(self._rc_login_error)
return [] return []
try: try:
@@ -166,6 +227,8 @@ class EmailVerifier:
def _rc_fetch_email_body(self, uid: int) -> Optional[str]: def _rc_fetch_email_body(self, uid: int) -> Optional[str]:
"""通过 Roundcube 读取指定 UID 邮件的正文""" """通过 Roundcube 读取指定 UID 邮件的正文"""
if not self._ensure_roundcube_session(): if not self._ensure_roundcube_session():
if self._rc_login_error:
raise EmailLoginError(self._rc_login_error)
return None return None
try: try:
@@ -288,6 +351,8 @@ class EmailVerifier:
if code: if code:
logger.success(f"获取到验证码: {code}") logger.success(f"获取到验证码: {code}")
return code return code
except EmailLoginError:
raise
except Exception as e: except Exception as e:
last_error = str(e) last_error = str(e)
logger.warning(f"Roundcube 读邮件异常: {e}") logger.warning(f"Roundcube 读邮件异常: {e}")
+4 -1
View File
@@ -11,7 +11,7 @@ from loguru import logger
from .cookie_enricher import CookieEnricher from .cookie_enricher import CookieEnricher
from .crypto import encrypt_password, encrypt_nickname_or_phone from .crypto import encrypt_password, encrypt_nickname_or_phone
from .email_verifier import EmailVerifier from .email_verifier import EmailLoginError, EmailVerifier
from .proxy_fetcher import ProxyFetcher from .proxy_fetcher import ProxyFetcher
from core.geetest.v3_slide.solver import ( from core.geetest.v3_slide.solver import (
@@ -310,6 +310,9 @@ class DouyuLogin:
except InterruptedError as e: except InterruptedError as e:
logger.warning(f"登录任务已停止: {e}") logger.warning(f"登录任务已停止: {e}")
return LoginResult(success=False, message=str(e)) return LoginResult(success=False, message=str(e))
except EmailLoginError as e:
logger.error(f"登录失败(邮箱登录失败,不再重试): {e}")
return LoginResult(success=False, message=str(e), code="email_login_failed")
except Exception as e: except Exception as e:
elapsed = time.monotonic() - start_time elapsed = time.monotonic() - start_time
if self.max_login_retries > 0: if self.max_login_retries > 0: