成功获取登录后的 ck
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"username": "用户9006787894",
|
||||||
|
"cookie": "dy_accounts_main=; dy_auth=e105BJzhxVWQ0tJKUXiiqN8ky11wgfj7ClW%2F8qo%2B1MPv8%2BDGHBq8VnuaJ1MWnm4EZqgVfLkM6PETWtGpLAHvadcMxCn9HWvTfT8%2BSKZR2dmsvReubJmeqBo; wan_auth37wan=4dc825c71d00LrMvodf%2B7ZrhnyMcsREgwQlgd7Ec06k5%2Bm%2FM0tdv0AJJ8gpoNoinSoPHS3vbJO8iK1vqiXfF2hTOLQqJOW3Wzs19DWLwpJpQgV8J%2BF0; LTP0=eyJhbGciOiJtZDUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsicGFzc3BvcnQiXSwiY3QiOjAsImN0aW1lIjoxNzgyMDU0MDA0LCJleHAiOjE3OTc4MjIwMTYsImtleSI6ImR5LWp3dC1tZDUiLCJsdGsiOiJhZDMxOTliZGRhYmM3ZWRmIiwibHRraWQiOjgwNTYxMzgyLCJzdWIiOiJsdCIsInVpZCI6OTg2Mzg1Mjk2fQ.Y2M0YTQ4YjQwZGYwZWI4YWZiZTllNmVjODNiZmJkMGE; last_login_way=nickname; PHPSESSID=ccr09aa2letju5662gdh5aih62; acf_auth=1a34kGp6rHjazzRy4fVzKXX11qCjC9RuQASDGOEfkRbLjsI3A5PzSCXawo5hZvO4g47LF9ruapNiMpf%2BE3u4TplWPMkW4c4eI0pyG05a%2F9dKr%2BOGM0hAEXk; acf_jwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk4NjM4NTI5NiwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImR5Iiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo4MDU2MTM4Miwic3RrIjoiNjFhODczZjUwZTg3M2Y5MCIsImV4cCI6MTc4MjY1ODgwNCwiaWF0IjoxNzgyMDU0MDA0LCJrZXkiOiJkeS1qd3QtbWQ1In0.OTFlMjIxOGU4MjZkMjcwMWM4NjQ1NTcwNjNmZWIzMDA; acf_dmjwt_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJtZDUifQ.eyJ1aWQiOjk4NjM4NTI5NiwiY3QiOjAsInN1YiI6InN0IiwiYXVkIjpbImRtIiwibm9uZSJdLCJiaXoiOjEsImx0a2lkIjo4MDU2MTM4Miwic3RrIjoiNjFhODczZjUwZTg3M2Y5MCIsImV4cCI6MTc4MjY1ODgwNCwiaWF0IjoxNzgyMDU0MDA0LCJrZXkiOiJkeS1qd3QtbWQ1In0.N2E0NmEyOWVlZDE1NTE4NDg0MTFlMmM5OGY1ZGQzZTk; acf_uid=986385296; acf_username=986385296; acf_nickname=%E7%94%A8%E6%88%B79006787894; acf_own_room=0; acf_groupid=1; acf_phonestatus=1; acf_avatar=https%3A%2F%2Fapic.douyucdn.cn%2Fupload%2Favatar%2Fdefault%2F25_; acf_ct=0; acf_ltkid=80561382; acf_biz=1; acf_stk=61a873f50e873f90; acf_devid=d53498d383f449f4c4ea28c4165aa2b4",
|
||||||
|
"timestamp": 1782054016
|
||||||
|
}
|
||||||
+183
-78
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import imaplib
|
import imaplib
|
||||||
import email
|
import email
|
||||||
|
import html
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from email.header import decode_header
|
from email.header import decode_header
|
||||||
|
from email.utils import parsedate_to_datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -20,12 +22,18 @@ class EmailVerifier:
|
|||||||
username: str,
|
username: str,
|
||||||
password: str,
|
password: str,
|
||||||
timeout: float = 12,
|
timeout: float = 12,
|
||||||
|
mailbox: str = "INBOX",
|
||||||
|
lookback_minutes: int = 10,
|
||||||
|
max_messages: int = 20,
|
||||||
):
|
):
|
||||||
self.imap_server = imap_server
|
self.imap_server = imap_server
|
||||||
self.imap_port = imap_port
|
self.imap_port = imap_port
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
self.mailbox = mailbox
|
||||||
|
self.lookback_minutes = lookback_minutes
|
||||||
|
self.max_messages = max_messages
|
||||||
self._connection: Optional[imaplib.IMAP4_SSL] = None
|
self._connection: Optional[imaplib.IMAP4_SSL] = None
|
||||||
|
|
||||||
def connect(self) -> None:
|
def connect(self) -> None:
|
||||||
@@ -52,13 +60,21 @@ class EmailVerifier:
|
|||||||
pass
|
pass
|
||||||
self._connection = None
|
self._connection = None
|
||||||
|
|
||||||
def get_verification_code(self, max_wait: int = 60, interval: int = 2) -> str:
|
def get_verification_code(
|
||||||
|
self,
|
||||||
|
max_wait: int = 60,
|
||||||
|
interval: int = 2,
|
||||||
|
after_timestamp: Optional[float] = None,
|
||||||
|
allow_old_seconds: int = 15,
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
获取斗鱼验证码
|
获取斗鱼验证码
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
max_wait: 最大等待时间(秒)
|
max_wait: 最大等待时间(秒)
|
||||||
interval: 轮询间隔(秒)
|
interval: 轮询间隔(秒)
|
||||||
|
after_timestamp: 发起发送验证码请求的时间戳,用于过滤旧邮件
|
||||||
|
allow_old_seconds: 邮件服务器时间允许向前偏移的秒数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
6位验证码
|
6位验证码
|
||||||
@@ -68,9 +84,12 @@ class EmailVerifier:
|
|||||||
self.connect()
|
self.connect()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
deadline = time.monotonic() + max_wait
|
||||||
while time.time() - start_time < max_wait:
|
while time.monotonic() < deadline:
|
||||||
code = self._fetch_latest_code()
|
code = self._fetch_latest_code(
|
||||||
|
after_timestamp=after_timestamp,
|
||||||
|
allow_old_seconds=allow_old_seconds,
|
||||||
|
)
|
||||||
if code:
|
if code:
|
||||||
logger.success(f"获取到验证码: {code}")
|
logger.success(f"获取到验证码: {code}")
|
||||||
return code
|
return code
|
||||||
@@ -82,56 +101,151 @@ class EmailVerifier:
|
|||||||
finally:
|
finally:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
|
||||||
def _fetch_latest_code(self) -> Optional[str]:
|
def _fetch_latest_code(
|
||||||
|
self,
|
||||||
|
after_timestamp: Optional[float] = None,
|
||||||
|
allow_old_seconds: int = 15,
|
||||||
|
) -> Optional[str]:
|
||||||
"""从IMAP获取最新验证码"""
|
"""从IMAP获取最新验证码"""
|
||||||
try:
|
try:
|
||||||
self._connection.select('INBOX')
|
if not self._connection:
|
||||||
|
raise RuntimeError("IMAP未连接")
|
||||||
|
|
||||||
# 搜索最近5分钟的邮件
|
status, _ = self._connection.select(self.mailbox, readonly=True)
|
||||||
since = (datetime.now() - timedelta(minutes=5)).strftime("%d-%b-%Y")
|
if status != 'OK':
|
||||||
status, messages = self._connection.search(
|
logger.warning(f"选择邮箱目录失败: {self.mailbox}")
|
||||||
None,
|
return None
|
||||||
f'(OR (FROM "douyu") (FROM "斗鱼") (SUBJECT "验证码")) SINCE {since}'
|
|
||||||
)
|
since = self._build_since_date(after_timestamp, allow_old_seconds)
|
||||||
|
# IMAP命令只能稳定发送ASCII条件,中文主题/发件人改到本地解析过滤。
|
||||||
|
status, messages = self._connection.search(None, 'SINCE', since)
|
||||||
|
|
||||||
if status != 'OK' or not messages[0]:
|
if status != 'OK' or not messages[0]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 获取最新的一封邮件
|
message_ids = messages[0].split()
|
||||||
latest_id = messages[0].split()[-1]
|
recent_ids = list(reversed(message_ids[-self.max_messages:]))
|
||||||
status, msg_data = self._connection.fetch(latest_id, '(RFC822)')
|
logger.debug(f"扫描最近 {len(recent_ids)} 封邮件,SINCE {since}")
|
||||||
|
|
||||||
if status != 'OK':
|
for message_id in recent_ids:
|
||||||
return None
|
msg = self._fetch_message(message_id)
|
||||||
|
if not msg:
|
||||||
|
continue
|
||||||
|
|
||||||
msg = email.message_from_bytes(msg_data[0][1])
|
if not self._is_recent_enough(msg, after_timestamp, allow_old_seconds):
|
||||||
|
continue
|
||||||
|
|
||||||
# 检查是否是斗鱼的邮件
|
subject = self._decode_subject(msg.get('Subject', ''))
|
||||||
subject = self._decode_subject(msg.get('Subject', ''))
|
from_addr = msg.get('From', '')
|
||||||
if not self._is_douyu_email(subject, msg.get('From', '')):
|
body = self._get_email_body(msg)
|
||||||
return None
|
|
||||||
|
|
||||||
# 提取验证码
|
if not self._is_douyu_email(subject, from_addr, body):
|
||||||
body = self._get_email_body(msg)
|
continue
|
||||||
code = self._extract_verification_code(body)
|
|
||||||
|
|
||||||
if code:
|
code = self._extract_verification_code(body)
|
||||||
logger.info(f"从邮件中提取到验证码: {code}")
|
if code:
|
||||||
|
logger.info(f"从邮件中提取到验证码,主题: {subject}")
|
||||||
|
return code
|
||||||
|
|
||||||
return code
|
return None
|
||||||
|
|
||||||
|
except imaplib.IMAP4.abort as e:
|
||||||
|
logger.warning(f"IMAP连接中断,准备下轮重连: {e}")
|
||||||
|
self.disconnect()
|
||||||
|
self.connect()
|
||||||
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取邮件失败: {e}")
|
logger.error(f"获取邮件失败: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _is_douyu_email(self, subject: str, from_addr: str) -> bool:
|
def _build_since_date(
|
||||||
"""判断是否是斗鱼的邮件"""
|
self,
|
||||||
douyu_keywords = ['斗鱼', 'douyu', '验证码', '安全验证']
|
after_timestamp: Optional[float],
|
||||||
subject_lower = subject.lower()
|
allow_old_seconds: int,
|
||||||
from_lower = from_addr.lower()
|
) -> str:
|
||||||
|
"""构造IMAP SINCE日期,月份固定用英文缩写。"""
|
||||||
|
if after_timestamp:
|
||||||
|
since_dt = datetime.fromtimestamp(
|
||||||
|
max(0, after_timestamp - allow_old_seconds)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
since_dt = datetime.now() - timedelta(minutes=self.lookback_minutes)
|
||||||
|
|
||||||
return any(keyword in subject_lower or keyword in from_lower
|
return self._format_imap_date(since_dt)
|
||||||
for keyword in douyu_keywords)
|
|
||||||
|
def _format_imap_date(self, value: datetime) -> str:
|
||||||
|
"""格式化IMAP日期,避免系统locale影响月份名称。"""
|
||||||
|
months = [
|
||||||
|
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||||
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
||||||
|
]
|
||||||
|
return f"{value.day:02d}-{months[value.month - 1]}-{value.year}"
|
||||||
|
|
||||||
|
def _fetch_message(self, message_id: bytes) -> Optional[email.message.Message]:
|
||||||
|
"""获取邮件完整内容,使用PEEK避免标记已读。"""
|
||||||
|
status, msg_data = self._connection.fetch(message_id, '(BODY.PEEK[])')
|
||||||
|
if status != 'OK':
|
||||||
|
return None
|
||||||
|
|
||||||
|
raw_message = self._join_fetch_payload(msg_data)
|
||||||
|
if not raw_message:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return email.message_from_bytes(raw_message)
|
||||||
|
|
||||||
|
def _join_fetch_payload(self, msg_data) -> bytes:
|
||||||
|
"""合并IMAP fetch返回中的邮件字节内容。"""
|
||||||
|
chunks = []
|
||||||
|
for item in msg_data:
|
||||||
|
if isinstance(item, tuple) and len(item) >= 2 and isinstance(item[1], bytes):
|
||||||
|
chunks.append(item[1])
|
||||||
|
elif isinstance(item, bytes) and item.startswith(b'From '):
|
||||||
|
chunks.append(item)
|
||||||
|
return b''.join(chunks)
|
||||||
|
|
||||||
|
def _is_recent_enough(
|
||||||
|
self,
|
||||||
|
msg: email.message.Message,
|
||||||
|
after_timestamp: Optional[float],
|
||||||
|
allow_old_seconds: int,
|
||||||
|
) -> bool:
|
||||||
|
"""检查邮件时间是否晚于本次发送验证码请求。"""
|
||||||
|
if not after_timestamp:
|
||||||
|
return True
|
||||||
|
|
||||||
|
message_time = self._parse_message_time(msg)
|
||||||
|
if not message_time:
|
||||||
|
logger.debug("邮件缺少Date头,保守纳入候选")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if message_time.timestamp() < after_timestamp - allow_old_seconds:
|
||||||
|
subject = self._decode_subject(msg.get('Subject', ''))
|
||||||
|
logger.debug(f"跳过旧邮件: {subject}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _parse_message_time(self, msg: email.message.Message) -> Optional[datetime]:
|
||||||
|
"""解析邮件Date头。"""
|
||||||
|
date_header = msg.get('Date')
|
||||||
|
if not date_header:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return parsedate_to_datetime(date_header)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _is_douyu_email(self, subject: str, from_addr: str, body: str = "") -> bool:
|
||||||
|
"""判断是否是斗鱼的邮件"""
|
||||||
|
douyu_keywords = ['斗鱼', 'douyu', 'douyutv']
|
||||||
|
verify_keywords = ['验证码', '安全验证', '登录验证', '动态码', '校验码']
|
||||||
|
combined = f"{subject}\n{from_addr}\n{body[:500]}".lower()
|
||||||
|
|
||||||
|
return (
|
||||||
|
any(keyword in combined for keyword in douyu_keywords)
|
||||||
|
or any(keyword in subject for keyword in verify_keywords)
|
||||||
|
)
|
||||||
|
|
||||||
def _decode_subject(self, subject: str) -> str:
|
def _decode_subject(self, subject: str) -> str:
|
||||||
"""解码邮件主题"""
|
"""解码邮件主题"""
|
||||||
@@ -149,72 +263,63 @@ class EmailVerifier:
|
|||||||
|
|
||||||
def _get_email_body(self, msg: email.message.Message) -> str:
|
def _get_email_body(self, msg: email.message.Message) -> str:
|
||||||
"""获取邮件正文"""
|
"""获取邮件正文"""
|
||||||
body = ""
|
parts = []
|
||||||
|
|
||||||
if msg.is_multipart():
|
if msg.is_multipart():
|
||||||
for part in msg.walk():
|
for part in msg.walk():
|
||||||
content_type = part.get_content_type()
|
content_type = part.get_content_type()
|
||||||
if content_type == 'text/plain' or content_type == 'text/html':
|
if part.get_content_disposition() == 'attachment':
|
||||||
try:
|
continue
|
||||||
payload = part.get_payload(decode=True)
|
|
||||||
charset = part.get_content_charset() or 'utf-8'
|
|
||||||
body += payload.decode(charset, errors='ignore')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
payload = msg.get_payload(decode=True)
|
|
||||||
charset = msg.get_content_charset() or 'utf-8'
|
|
||||||
body = payload.decode(charset, errors='ignore')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return body
|
if content_type == 'text/plain' or content_type == 'text/html':
|
||||||
|
decoded = self._decode_part_payload(part)
|
||||||
|
if decoded:
|
||||||
|
parts.append(decoded)
|
||||||
|
else:
|
||||||
|
decoded = self._decode_part_payload(msg)
|
||||||
|
if decoded:
|
||||||
|
parts.append(decoded)
|
||||||
|
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
def _decode_part_payload(self, part: email.message.Message) -> str:
|
||||||
|
"""解码邮件片段内容。"""
|
||||||
|
try:
|
||||||
|
payload = part.get_payload(decode=True)
|
||||||
|
if payload is None:
|
||||||
|
raw_payload = part.get_payload()
|
||||||
|
return raw_payload if isinstance(raw_payload, str) else ""
|
||||||
|
|
||||||
|
charset = part.get_content_charset() or 'utf-8'
|
||||||
|
return payload.decode(charset, errors='ignore')
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
def _extract_verification_code(self, text: str) -> Optional[str]:
|
def _extract_verification_code(self, text: str) -> Optional[str]:
|
||||||
"""从文本中提取验证码"""
|
"""从文本中提取验证码"""
|
||||||
# 清理HTML标签
|
# 清理HTML标签和实体,方便匹配中文邮件模板。
|
||||||
|
text = html.unescape(text)
|
||||||
text = re.sub(r'<[^>]+>', ' ', text)
|
text = re.sub(r'<[^>]+>', ' ', text)
|
||||||
|
text = re.sub(r'\s+', ' ', text)
|
||||||
|
|
||||||
# 查找6位数字验证码
|
# 查找6位数字验证码
|
||||||
# 注意:中文冒号和英文冒号需要分别处理
|
# 注意:中文冒号和英文冒号需要分别处理
|
||||||
patterns = [
|
patterns = [
|
||||||
r'验证码[::]\s*(\d{6})', # 验证码:123456 或 验证码:123456
|
r'(?:验证码|校验码|动态码|安全码)\s*(?:是|为|:|:)?\s*(\d{6})',
|
||||||
r'验证码\s+(\d{6})', # 验证码 123456
|
r'(?:您的|你的|本次)?(?:验证码|校验码|动态码|安全码)\s*(?:是|为)?\s*(\d{6})',
|
||||||
r'(\d{6})\s*是您的验证码', # 123456是您的验证码
|
r'(\d{6})\s*(?:是|为)?(?:您的|你的|本次)?(?:验证码|校验码|动态码|安全码)',
|
||||||
r'您的验证码[是为]\s*[::]\s*(\d{6})', # 您的验证码是:123456
|
r'(?:verification\s+code|security\s+code|code)\s*(?:is|:|:)?\s*(\d{6})',
|
||||||
r'您的验证码[是为]\s*(\d{6})', # 您的验证码是123456
|
r'\b(\d{6})\b', # 候选邮件已经过滤为斗鱼验证码邮件,最后再兜底匹配6位数字。
|
||||||
r'verification code[::]\s*(\d{6})',
|
|
||||||
r'code[::]\s*(\d{6})',
|
|
||||||
r'(\d{6})', # 最后尝试匹配任何6位数字
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for pattern in patterns:
|
for pattern in patterns:
|
||||||
match = re.search(pattern, text, re.IGNORECASE)
|
match = re.search(pattern, text, re.IGNORECASE)
|
||||||
if match:
|
if match:
|
||||||
code = match.group(1)
|
code = match.group(1)
|
||||||
# 验证码有效性检查(简单过滤明显不是验证码的数字)
|
|
||||||
if not self._is_likely_code(code):
|
|
||||||
continue
|
|
||||||
return code
|
return code
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _is_likely_code(self, code: str) -> bool:
|
|
||||||
"""判断是否是有效的验证码"""
|
|
||||||
# 过滤掉一些明显不是验证码的数字
|
|
||||||
invalid_patterns = [
|
|
||||||
r'^(\d)\1{5}$', # 全部相同:111111, 222222
|
|
||||||
r'^123456$', # 顺序数字
|
|
||||||
r'^654321$', # 逆序数字
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in invalid_patterns:
|
|
||||||
if re.match(pattern, code):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def get_email_config_for_account(email_address: str) -> dict:
|
def get_email_config_for_account(email_address: str) -> dict:
|
||||||
"""根据邮箱地址自动配置IMAP服务器"""
|
"""根据邮箱地址自动配置IMAP服务器"""
|
||||||
|
|||||||
+7
-3
@@ -222,11 +222,12 @@ class DouyuLogin:
|
|||||||
|
|
||||||
# 4️⃣ 发送邮箱验证
|
# 4️⃣ 发送邮箱验证
|
||||||
logger.info("步骤4: 发送邮箱验证...")
|
logger.info("步骤4: 发送邮箱验证...")
|
||||||
|
email_sent_at = time.time()
|
||||||
self._send_email_verify(remote_code)
|
self._send_email_verify(remote_code)
|
||||||
|
|
||||||
# 5️⃣ IMAP获取验证码
|
# 5️⃣ IMAP获取验证码
|
||||||
logger.info("步骤5: 获取邮箱验证码...")
|
logger.info("步骤5: 获取邮箱验证码...")
|
||||||
verify_code = self._get_email_code()
|
verify_code = self._get_email_code(after_timestamp=email_sent_at)
|
||||||
|
|
||||||
# 6️⃣ 提交验证码
|
# 6️⃣ 提交验证码
|
||||||
logger.info("步骤6: 提交验证码...")
|
logger.info("步骤6: 提交验证码...")
|
||||||
@@ -455,7 +456,7 @@ class DouyuLogin:
|
|||||||
|
|
||||||
logger.info("验证邮件已发送")
|
logger.info("验证邮件已发送")
|
||||||
|
|
||||||
def _get_email_code(self) -> str:
|
def _get_email_code(self, after_timestamp: Optional[float] = None) -> str:
|
||||||
"""获取邮箱验证码"""
|
"""获取邮箱验证码"""
|
||||||
verifier = EmailVerifier(
|
verifier = EmailVerifier(
|
||||||
imap_server=self.account.email_imap_server,
|
imap_server=self.account.email_imap_server,
|
||||||
@@ -464,7 +465,10 @@ class DouyuLogin:
|
|||||||
password=self.account.email_password,
|
password=self.account.email_password,
|
||||||
)
|
)
|
||||||
|
|
||||||
return verifier.get_verification_code(max_wait=60)
|
return verifier.get_verification_code(
|
||||||
|
max_wait=60,
|
||||||
|
after_timestamp=after_timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
def _submit_verify_code(self, remote_code: str, verify_code: str) -> str:
|
def _submit_verify_code(self, remote_code: str, verify_code: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""邮箱IMAP读取测试脚本"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 添加项目根目录到path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
from douyu.config import Config
|
||||||
|
from douyu.email_verifier import EmailVerifier
|
||||||
|
from utils.logger import setup_logger
|
||||||
|
|
||||||
|
|
||||||
|
def mask_email(email_address: str) -> str:
|
||||||
|
"""隐藏邮箱中间部分,避免控制台泄露完整账号。"""
|
||||||
|
if "@" not in email_address:
|
||||||
|
return email_address
|
||||||
|
|
||||||
|
name, domain = email_address.split("@", 1)
|
||||||
|
if len(name) <= 2:
|
||||||
|
masked_name = name[0] + "*"
|
||||||
|
else:
|
||||||
|
masked_name = name[:2] + "*" * max(2, len(name) - 4) + name[-2:]
|
||||||
|
|
||||||
|
return f"{masked_name}@{domain}"
|
||||||
|
|
||||||
|
|
||||||
|
def clean_preview(text: str, max_length: int = 120) -> str:
|
||||||
|
"""清理正文预览,保持输出紧凑。"""
|
||||||
|
preview = " ".join(text.split())
|
||||||
|
preview = re.sub(r"\b\d{6}\b", "******", preview)
|
||||||
|
if len(preview) <= max_length:
|
||||||
|
return preview
|
||||||
|
return preview[:max_length] + "..."
|
||||||
|
|
||||||
|
|
||||||
|
def format_code(code: str, show_code: bool) -> str:
|
||||||
|
"""格式化验证码输出,默认隐藏大部分数字。"""
|
||||||
|
if not code:
|
||||||
|
return "未提取到"
|
||||||
|
if show_code:
|
||||||
|
return code
|
||||||
|
return f"****{code[-2:]}"
|
||||||
|
|
||||||
|
|
||||||
|
def format_message_time(verifier: EmailVerifier, msg) -> str:
|
||||||
|
"""格式化邮件时间。"""
|
||||||
|
message_time = verifier._parse_message_time(msg)
|
||||||
|
if not message_time:
|
||||||
|
return "未知"
|
||||||
|
return message_time.strftime("%Y-%m-%d %H:%M:%S %z")
|
||||||
|
|
||||||
|
|
||||||
|
def load_account(config_path: str, index: int):
|
||||||
|
"""从配置文件加载指定账号。"""
|
||||||
|
config = Config(config_path)
|
||||||
|
accounts = config.get_accounts()
|
||||||
|
if not accounts:
|
||||||
|
raise ValueError("config.yaml 中没有配置账号")
|
||||||
|
|
||||||
|
if index < 0 or index >= len(accounts):
|
||||||
|
raise IndexError(f"账号索引超出范围: {index},当前共有 {len(accounts)} 个账号")
|
||||||
|
|
||||||
|
return accounts[index]
|
||||||
|
|
||||||
|
|
||||||
|
def list_recent_messages(
|
||||||
|
verifier: EmailVerifier,
|
||||||
|
lookback_minutes: int,
|
||||||
|
limit: int,
|
||||||
|
show_code: bool,
|
||||||
|
) -> None:
|
||||||
|
"""列出最近邮件,并测试斗鱼验证码解析。"""
|
||||||
|
verifier.connect()
|
||||||
|
|
||||||
|
try:
|
||||||
|
status, _ = verifier._connection.select(verifier.mailbox, readonly=True)
|
||||||
|
if status != "OK":
|
||||||
|
raise RuntimeError(f"选择邮箱目录失败: {verifier.mailbox}")
|
||||||
|
|
||||||
|
since_dt = datetime.now() - timedelta(minutes=lookback_minutes)
|
||||||
|
since = verifier._format_imap_date(since_dt)
|
||||||
|
status, messages = verifier._connection.search(None, "SINCE", since)
|
||||||
|
if status != "OK":
|
||||||
|
raise RuntimeError(f"搜索邮件失败: status={status}")
|
||||||
|
|
||||||
|
message_ids = messages[0].split() if messages and messages[0] else []
|
||||||
|
if not message_ids:
|
||||||
|
print(f"最近 {lookback_minutes} 分钟没有邮件")
|
||||||
|
return
|
||||||
|
|
||||||
|
recent_ids = list(reversed(message_ids[-limit:]))
|
||||||
|
print(f"搜索范围: 最近 {lookback_minutes} 分钟")
|
||||||
|
print(f"匹配邮件数: {len(message_ids)},展示最新 {len(recent_ids)} 封\n")
|
||||||
|
|
||||||
|
for index, message_id in enumerate(recent_ids, start=1):
|
||||||
|
msg = verifier._fetch_message(message_id)
|
||||||
|
if not msg:
|
||||||
|
print(f"[{index}] 读取失败: id={message_id.decode(errors='ignore')}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
subject = verifier._decode_subject(msg.get("Subject", ""))
|
||||||
|
from_addr = msg.get("From", "")
|
||||||
|
body = verifier._get_email_body(msg)
|
||||||
|
is_douyu = verifier._is_douyu_email(subject, from_addr, body)
|
||||||
|
code = verifier._extract_verification_code(body) if is_douyu else ""
|
||||||
|
|
||||||
|
print(f"[{index}] 邮件ID: {message_id.decode(errors='ignore')}")
|
||||||
|
print(f" 时间: {format_message_time(verifier, msg)}")
|
||||||
|
print(f" 发件人: {from_addr}")
|
||||||
|
print(f" 主题: {subject}")
|
||||||
|
print(f" 斗鱼邮件: {'是' if is_douyu else '否'}")
|
||||||
|
print(f" 验证码: {format_code(code, show_code)}")
|
||||||
|
print(f" 正文预览: {clean_preview(body)}\n")
|
||||||
|
finally:
|
||||||
|
verifier.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def wait_verification_code(
|
||||||
|
account,
|
||||||
|
max_wait: int,
|
||||||
|
interval: int,
|
||||||
|
new_only: bool,
|
||||||
|
show_code: bool,
|
||||||
|
) -> None:
|
||||||
|
"""等待并提取斗鱼验证码,不负责发送验证码邮件。"""
|
||||||
|
verifier = EmailVerifier(
|
||||||
|
imap_server=account.email_imap_server,
|
||||||
|
imap_port=account.email_imap_port,
|
||||||
|
username=account.email,
|
||||||
|
password=account.email_password,
|
||||||
|
)
|
||||||
|
|
||||||
|
after_timestamp = time.time() if new_only else None
|
||||||
|
code = verifier.get_verification_code(
|
||||||
|
max_wait=max_wait,
|
||||||
|
interval=interval,
|
||||||
|
after_timestamp=after_timestamp,
|
||||||
|
)
|
||||||
|
print(f"等待验证码结果: {format_code(code, show_code)}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
"""解析命令行参数。"""
|
||||||
|
parser = argparse.ArgumentParser(description="测试邮箱IMAP是否能读取邮件和验证码")
|
||||||
|
parser.add_argument("-c", "--config", default="config.yaml", help="配置文件路径")
|
||||||
|
parser.add_argument("-i", "--index", type=int, default=0, help="账号索引")
|
||||||
|
parser.add_argument("--lookback-minutes", type=int, default=30, help="读取最近多少分钟邮件")
|
||||||
|
parser.add_argument("--limit", type=int, default=5, help="展示最近多少封邮件")
|
||||||
|
parser.add_argument("--wait-code", action="store_true", help="额外等待并提取斗鱼验证码")
|
||||||
|
parser.add_argument("--new-only", action="store_true", help="等待验证码时只接受脚本启动后的新邮件")
|
||||||
|
parser.add_argument("--show-code", action="store_true", help="输出完整验证码")
|
||||||
|
parser.add_argument("--max-wait", type=int, default=60, help="等待验证码最大秒数")
|
||||||
|
parser.add_argument("--interval", type=int, default=2, help="验证码轮询间隔秒数")
|
||||||
|
parser.add_argument("--log-level", default="INFO", help="日志级别")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""脚本入口。"""
|
||||||
|
args = parse_args()
|
||||||
|
setup_logger(level=args.log_level)
|
||||||
|
|
||||||
|
account = load_account(args.config, args.index)
|
||||||
|
print("=== 邮箱IMAP读取测试 ===")
|
||||||
|
print(f"账号索引: {args.index}")
|
||||||
|
print(f"邮箱账号: {mask_email(account.email)}")
|
||||||
|
print(f"IMAP服务器: {account.email_imap_server}:{account.email_imap_port}\n")
|
||||||
|
|
||||||
|
verifier = EmailVerifier(
|
||||||
|
imap_server=account.email_imap_server,
|
||||||
|
imap_port=account.email_imap_port,
|
||||||
|
username=account.email,
|
||||||
|
password=account.email_password,
|
||||||
|
lookback_minutes=args.lookback_minutes,
|
||||||
|
max_messages=args.limit,
|
||||||
|
)
|
||||||
|
list_recent_messages(
|
||||||
|
verifier=verifier,
|
||||||
|
lookback_minutes=args.lookback_minutes,
|
||||||
|
limit=args.limit,
|
||||||
|
show_code=args.show_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.wait_code:
|
||||||
|
print("=== 等待斗鱼验证码 ===")
|
||||||
|
wait_verification_code(
|
||||||
|
account=account,
|
||||||
|
max_wait=args.max_wait,
|
||||||
|
interval=args.interval,
|
||||||
|
new_only=args.new_only,
|
||||||
|
show_code=args.show_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user