优化邮箱登录错误分支,增加备用密码
This commit is contained in:
@@ -14,6 +14,15 @@ import requests
|
||||
|
||||
# Roundcube Webmail 地址。可通过环境变量覆盖。空值时回退到默认。
|
||||
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:
|
||||
@@ -31,6 +40,7 @@ class EmailVerifier:
|
||||
max_messages: int = 20,
|
||||
use_ssl: bool = False,
|
||||
roundcube_url: str = "",
|
||||
backup_passwords: Optional[list[str]] = None,
|
||||
):
|
||||
# 兼容旧参数名
|
||||
self.imap_server = imap_server
|
||||
@@ -39,11 +49,13 @@ 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
|
||||
|
||||
# Roundcube 会话(懒初始化)
|
||||
self._rc_session: Optional[requests.Session] = None
|
||||
self._rc_token: Optional[str] = None
|
||||
self._rc_logged_in = False
|
||||
self._rc_login_error = ""
|
||||
|
||||
def connect(self) -> None:
|
||||
"""兼容接口"""
|
||||
@@ -69,6 +81,39 @@ class EmailVerifier:
|
||||
if self._rc_logged_in and self._rc_session and self._rc_token:
|
||||
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:
|
||||
session = requests.Session()
|
||||
session.headers.update({
|
||||
@@ -80,7 +125,7 @@ class EmailVerifier:
|
||||
token_match = re.search(r'name="_token"\s+value="([^"]+)"', resp.text)
|
||||
if not token_match:
|
||||
logger.warning("Roundcube: 未获取到登录 token")
|
||||
return False
|
||||
return False, False
|
||||
token = token_match.group(1)
|
||||
|
||||
# 2. 登录
|
||||
@@ -91,27 +136,41 @@ class EmailVerifier:
|
||||
"_task": "login",
|
||||
"_action": "login",
|
||||
"_user": self.username,
|
||||
"_pass": self.password,
|
||||
"_pass": password,
|
||||
},
|
||||
timeout=self.timeout,
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
# 3. 检查登录是否成功(成功会跳到 ?_task=mail)
|
||||
# 3. 检查登录是否成功:登录失败页也可能有 request_token,不能只靠 token 判定。
|
||||
new_token_match = re.search(r'request_token["\s:]+["\']([^"\']+)', resp.text)
|
||||
if not new_token_match:
|
||||
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_token = new_token_match.group(1)
|
||||
self._rc_logged_in = True
|
||||
logger.debug(f"Roundcube: 登录成功 ({self.username})")
|
||||
return True
|
||||
password_name = "备用邮箱密码" if is_backup else "主邮箱密码"
|
||||
logger.debug(f"Roundcube: {password_name}登录成功 ({self.username})")
|
||||
return True, False
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Roundcube: 登录异常: {e}")
|
||||
return False
|
||||
return False, False
|
||||
|
||||
# ── Roundcube 邮件列表(含精确日期) ────────────────────
|
||||
|
||||
@@ -121,6 +180,8 @@ class EmailVerifier:
|
||||
返回 [{"uid": 2, "subject": "验证码 - 斗鱼", "date": "今天 09:02"}, ...]
|
||||
"""
|
||||
if not self._ensure_roundcube_session():
|
||||
if self._rc_login_error:
|
||||
raise EmailLoginError(self._rc_login_error)
|
||||
return []
|
||||
|
||||
try:
|
||||
@@ -166,6 +227,8 @@ class EmailVerifier:
|
||||
def _rc_fetch_email_body(self, uid: int) -> Optional[str]:
|
||||
"""通过 Roundcube 读取指定 UID 邮件的正文"""
|
||||
if not self._ensure_roundcube_session():
|
||||
if self._rc_login_error:
|
||||
raise EmailLoginError(self._rc_login_error)
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -288,6 +351,8 @@ class EmailVerifier:
|
||||
if code:
|
||||
logger.success(f"获取到验证码: {code}")
|
||||
return code
|
||||
except EmailLoginError:
|
||||
raise
|
||||
except Exception as e:
|
||||
last_error = str(e)
|
||||
logger.warning(f"Roundcube 读邮件异常: {e}")
|
||||
|
||||
+4
-1
@@ -11,7 +11,7 @@ from loguru import logger
|
||||
|
||||
from .cookie_enricher import CookieEnricher
|
||||
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 core.geetest.v3_slide.solver import (
|
||||
@@ -310,6 +310,9 @@ class DouyuLogin:
|
||||
except InterruptedError as e:
|
||||
logger.warning(f"登录任务已停止: {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:
|
||||
elapsed = time.monotonic() - start_time
|
||||
if self.max_login_retries > 0:
|
||||
|
||||
Reference in New Issue
Block a user