"""邮箱验证模块 - 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})