Files
live-hub-py/douyu/email_verifier.py
T
2026-06-21 21:56:56 +08:00

232 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""邮箱验证模块 - IMAP获取验证码"""
import imaplib
import email
import re
import time
from datetime import datetime, timedelta
from email.header import decode_header
from typing import Optional
from loguru import logger
class EmailVerifier:
"""邮箱验证器"""
def __init__(
self,
imap_server: str,
imap_port: int,
username: str,
password: str,
timeout: float = 12,
):
self.imap_server = imap_server
self.imap_port = imap_port
self.username = username
self.password = password
self.timeout = timeout
self._connection: Optional[imaplib.IMAP4_SSL] = None
def connect(self) -> None:
"""连接IMAP服务器"""
try:
logger.info(f"连接IMAP服务器: {self.imap_server}:{self.imap_port}")
self._connection = imaplib.IMAP4_SSL(
self.imap_server,
self.imap_port,
timeout=self.timeout,
)
self._connection.login(self.username, self.password)
logger.info("IMAP连接成功")
except Exception as e:
logger.error(f"IMAP连接失败: {e}")
raise
def disconnect(self) -> None:
"""断开IMAP连接"""
if self._connection:
try:
self._connection.logout()
except:
pass
self._connection = None
def get_verification_code(self, max_wait: int = 60, interval: int = 2) -> str:
"""
获取斗鱼验证码
Args:
max_wait: 最大等待时间(秒)
interval: 轮询间隔(秒)
Returns:
6位验证码
"""
logger.info(f"等待斗鱼验证码邮件,最大等待 {max_wait} 秒...")
self.connect()
try:
start_time = time.time()
while time.time() - start_time < max_wait:
code = self._fetch_latest_code()
if code:
logger.success(f"获取到验证码: {code}")
return code
logger.debug("未找到验证码,等待中...")
time.sleep(interval)
raise TimeoutError("等待验证码超时")
finally:
self.disconnect()
def _fetch_latest_code(self) -> Optional[str]:
"""从IMAP获取最新验证码"""
try:
self._connection.select('INBOX')
# 搜索最近5分钟的邮件
since = (datetime.now() - timedelta(minutes=5)).strftime("%d-%b-%Y")
status, messages = self._connection.search(
None,
f'(OR (FROM "douyu") (FROM "斗鱼") (SUBJECT "验证码")) SINCE {since}'
)
if status != 'OK' or not messages[0]:
return None
# 获取最新的一封邮件
latest_id = messages[0].split()[-1]
status, msg_data = self._connection.fetch(latest_id, '(RFC822)')
if status != 'OK':
return None
msg = email.message_from_bytes(msg_data[0][1])
# 检查是否是斗鱼的邮件
subject = self._decode_subject(msg.get('Subject', ''))
if not self._is_douyu_email(subject, msg.get('From', '')):
return None
# 提取验证码
body = self._get_email_body(msg)
code = self._extract_verification_code(body)
if code:
logger.info(f"从邮件中提取到验证码: {code}")
return code
except Exception as e:
logger.error(f"获取邮件失败: {e}")
return None
def _is_douyu_email(self, subject: str, from_addr: str) -> bool:
"""判断是否是斗鱼的邮件"""
douyu_keywords = ['斗鱼', 'douyu', '验证码', '安全验证']
subject_lower = subject.lower()
from_lower = from_addr.lower()
return any(keyword in subject_lower or keyword in from_lower
for keyword in douyu_keywords)
def _decode_subject(self, subject: str) -> str:
"""解码邮件主题"""
if not subject:
return ""
decoded_parts = decode_header(subject)
result = []
for part, charset in decoded_parts:
if isinstance(part, bytes):
result.append(part.decode(charset or 'utf-8', errors='ignore'))
else:
result.append(part)
return ' '.join(result)
def _get_email_body(self, msg: email.message.Message) -> str:
"""获取邮件正文"""
body = ""
if msg.is_multipart():
for part in msg.walk():
content_type = part.get_content_type()
if content_type == 'text/plain' or content_type == 'text/html':
try:
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
def _extract_verification_code(self, text: str) -> Optional[str]:
"""从文本中提取验证码"""
# 清理HTML标签
text = re.sub(r'<[^>]+>', ' ', text)
# 查找6位数字验证码
# 注意:中文冒号和英文冒号需要分别处理
patterns = [
r'验证码[:]\s*(\d{6})', # 验证码:123456 或 验证码:123456
r'验证码\s+(\d{6})', # 验证码 123456
r'(\d{6})\s*是您的验证码', # 123456是您的验证码
r'您的验证码[是为]\s*[:]\s*(\d{6})', # 您的验证码是:123456
r'您的验证码[是为]\s*(\d{6})', # 您的验证码是123456
r'verification code[:]\s*(\d{6})',
r'code[:]\s*(\d{6})',
r'(\d{6})', # 最后尝试匹配任何6位数字
]
for pattern in patterns:
match = re.search(pattern, text, re.IGNORECASE)
if match:
code = match.group(1)
# 验证码有效性检查(简单过滤明显不是验证码的数字)
if not self._is_likely_code(code):
continue
return code
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:
"""根据邮箱地址自动配置IMAP服务器"""
configs = {
'qq.com': {'server': 'imap.qq.com', 'port': 993},
'163.com': {'server': 'imap.163.com', 'port': 993},
'126.com': {'server': 'imap.126.com', 'port': 993},
'gmail.com': {'server': 'imap.gmail.com', 'port': 993},
'outlook.com': {'server': 'outlook.office365.com', 'port': 993},
'hotmail.com': {'server': 'outlook.office365.com', 'port': 993},
}
domain = email_address.split('@')[-1].lower()
return configs.get(domain, {'server': f'imap.{domain}', 'port': 993})