优化邮箱登录错误分支,增加备用密码
This commit is contained in:
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user