Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/core/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,10 @@ def save_to_database(self, result: RegistrationResult) -> bool:
try:
# 获取默认 client_id
settings = get_settings()
metadata = dict(result.metadata or {})
verification_state = self.email_service.export_verification_state(result.email or self.email)
if verification_state["used_codes"] or verification_state["seen_messages"]:
metadata["verification_state"] = verification_state

with get_db() as db:
# 保存账户信息
Expand All @@ -1699,7 +1703,7 @@ def save_to_database(self, result: RegistrationResult) -> bool:
id_token=result.id_token,
cookies=result.cookies,
proxy_used=self.proxy_url,
extra_data=result.metadata,
extra_data=metadata,
source=result.source
)

Expand Down
137 changes: 137 additions & 0 deletions src/services/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import re
import time
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, Dict, Any, List
from enum import Enum

Expand Down Expand Up @@ -146,6 +147,8 @@ def __init__(self, service_type: EmailServiceType, name: str = None):
self._status = EmailServiceStatus.HEALTHY
self._last_error = None
self._provider_backoff = reset_adaptive_backoff()
self._used_verification_codes: Dict[str, set] = {}
self._seen_verification_messages: Dict[str, set] = {}

_EMAIL_ADDRESS_PATTERN = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")

Expand Down Expand Up @@ -299,6 +302,140 @@ def _extract_otp_from_text(self, text: str, pattern: Optional[str] = None) -> Op

return None

def _get_used_verification_codes(self, email: str) -> set:
"""获取邮箱对应的已使用验证码集合。"""
key = str(email or "").strip().lower()
if key not in self._used_verification_codes:
self._used_verification_codes[key] = set()
return self._used_verification_codes[key]

def _get_seen_verification_messages(self, email: str) -> set:
"""获取邮箱对应的已处理消息标识集合。"""
key = str(email or "").strip().lower()
if key not in self._seen_verification_messages:
self._seen_verification_messages[key] = set()
return self._seen_verification_messages[key]

def load_verification_state(
self,
email: str,
used_codes: Optional[List[str]] = None,
seen_messages: Optional[List[str]] = None,
) -> None:
"""将持久化的验证码状态恢复到当前服务实例。"""
if used_codes:
self._get_used_verification_codes(email).update(
str(code) for code in used_codes if code
)
if seen_messages:
self._get_seen_verification_messages(email).update(
str(marker) for marker in seen_messages if marker
)

def export_verification_state(self, email: str) -> Dict[str, List[str]]:
"""导出当前邮箱的验证码状态,用于跨请求复用。"""
return {
"used_codes": sorted(self._get_used_verification_codes(email)),
"seen_messages": sorted(self._get_seen_verification_messages(email)),
}

def _remember_verification_code(self, email: str, code: str) -> bool:
"""记录验证码;若已用过则返回 False。"""
used_codes = self._get_used_verification_codes(email)
if code in used_codes:
return False
used_codes.add(code)
return True

def _remember_verification_message(self, email: str, message_marker: Optional[str]) -> bool:
"""记录消息标识;若已处理过则返回 False。"""
if not message_marker:
return True

seen_messages = self._get_seen_verification_messages(email)
if message_marker in seen_messages:
return False
seen_messages.add(message_marker)
return True

def _accept_verification_code(
self,
email: str,
code: str,
message_marker: Optional[str] = None,
) -> bool:
"""
决定是否接受验证码。

若有可靠的新邮件标识,优先按消息去重,这样新邮件即便验证码重复也能被接受;
否则退回到按验证码去重,避免旧码被重复消费。
"""
if message_marker:
if not self._remember_verification_message(email, message_marker):
return False
self._get_used_verification_codes(email).add(code)
return True

return self._remember_verification_code(email, code)

def _parse_message_timestamp(self, value: Any) -> Optional[float]:
"""将常见邮件时间字段解析为 Unix 时间戳。"""
if value is None or value == "":
return None

if isinstance(value, datetime):
return value.timestamp()

if isinstance(value, (int, float)):
return self._normalize_unix_timestamp(float(value))

text = str(value).strip()
if not text:
return None

try:
return self._normalize_unix_timestamp(float(text))
except ValueError:
pass

normalized = text.replace("Z", "+00:00") if text.endswith("Z") else text
try:
return datetime.fromisoformat(normalized).timestamp()
except ValueError:
return None

def _normalize_unix_timestamp(self, value: float) -> float:
"""将秒/毫秒/微秒级 Unix 时间统一归一到秒。"""
absolute = abs(value)
if absolute >= 1e14:
return value / 1_000_000
if absolute >= 1e11:
return value / 1_000
return value

def _is_message_before_otp(self, message_time: Any, otp_sent_at: Optional[float], tolerance_seconds: int = 1) -> bool:
"""
判断邮件是否早于当前 OTP 发送窗口。

允许少量时钟误差,避免接口时间与本地时间有轻微偏移时误伤新邮件。
"""
if not otp_sent_at:
return False

message_ts = self._parse_message_timestamp(message_time)
if message_ts is None:
return False

return message_ts + tolerance_seconds < otp_sent_at

def _sort_items_by_message_time(self, items: List[Any], value_getter) -> List[Any]:
"""按邮件时间倒序排列,优先处理最新邮件。"""
return sorted(
items,
key=lambda item: self._parse_message_timestamp(value_getter(item)) or float("-inf"),
reverse=True,
)

def wait_for_email(
self,
email: str,
Expand Down
13 changes: 11 additions & 2 deletions src/services/duck_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,12 @@ def get_verification_code(
)
messages = response.get("hydra:member", [])

for message in messages:
ordered_messages = self._sort_items_by_message_time(
messages,
lambda item: item.get("createdAt") if isinstance(item, dict) else None,
)

for message in ordered_messages:
message_id = str(message.get("id") or "").strip()
if not message_id or message_id in seen_message_ids:
continue
Expand All @@ -281,6 +286,7 @@ def get_verification_code(
continue

seen_message_ids.add(message_id)
message_marker = f"id:{message_id}"
detail = self._make_request(
"GET",
f"/messages/{message_id}",
Expand All @@ -293,8 +299,11 @@ def get_verification_code(

match = re.search(pattern, content)
if match:
code = match.group(1)
if not self._accept_verification_code(email, code, message_marker):
continue
self.update_status(True)
return match.group(1)
return code
except Exception as e:
logger.debug(f"DuckMail 轮询验证码失败: {e}")

Expand Down
28 changes: 25 additions & 3 deletions src/services/freemail.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,29 @@ def get_verification_code(
time.sleep(3)
continue

for mail in mails:
ordered_mails = self._sort_items_by_message_time(
mails,
lambda item: (
item.get("created_at")
or item.get("createdAt")
or item.get("received_at")
or item.get("receivedAt")
) if isinstance(item, dict) else None,
)

for mail in ordered_mails:
mail_id = mail.get("id")
if not mail_id or mail_id in seen_mail_ids:
continue

seen_mail_ids.add(mail_id)
message_marker = f"id:{mail_id}"

if self._is_message_before_otp(
mail.get("created_at") or mail.get("createdAt") or mail.get("received_at") or mail.get("receivedAt"),
otp_sent_at,
):
continue

sender = str(mail.get("sender", "")).lower()
subject = str(mail.get("subject", ""))
Expand All @@ -239,25 +256,30 @@ def get_verification_code(

code = self._extract_otp_from_text(content, pattern)
if code:
if not self._accept_verification_code(email, code, message_marker):
continue
logger.info(f"从 Freemail 邮箱 {email} 找到验证码: {code}")
self.update_status(True)
return code

v_code = str(mail.get("verification_code") or "").strip()

# 如果依然未找到,获取邮件详情进行匹配
try:
detail = self._make_request("GET", f"/api/email/{mail_id}")
full_content = str(detail.get("content", "")) + "\n" + str(detail.get("html_content", ""))
code = self._extract_otp_from_text(full_content, pattern)
if code:
if not self._accept_verification_code(email, code, message_marker):
continue
logger.info(f"从 Freemail 邮箱 {email} 找到验证码: {code}")
self.update_status(True)
return code
except Exception as e:
logger.debug(f"获取 Freemail 邮件详情失败: {e}")

v_code = str(mail.get("verification_code") or "").strip()
if re.fullmatch(r"\d{6}", v_code):
if not self._accept_verification_code(email, v_code, message_marker):
continue
logger.info(f"从 Freemail 邮箱 {email} 找到验证码: {v_code}")
self.update_status(True)
return v_code
Expand Down
21 changes: 20 additions & 1 deletion src/services/moe_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,12 +316,29 @@ def get_verification_code(
time.sleep(3)
continue

for message in messages:
ordered_messages = self._sort_items_by_message_time(
messages,
lambda item: (
item.get("created_at")
or item.get("createdAt")
or item.get("received_at")
or item.get("receivedAt")
) if isinstance(item, dict) else None,
)

for message in ordered_messages:
message_id = message.get("id")
if not message_id or message_id in seen_message_ids:
continue

seen_message_ids.add(message_id)
message_marker = f"id:{message_id}"

if self._is_message_before_otp(
message.get("created_at") or message.get("createdAt") or message.get("received_at") or message.get("receivedAt"),
otp_sent_at,
):
continue

# 检查是否是目标邮件
sender = str(message.get("from_address", "")).lower()
Expand All @@ -343,6 +360,8 @@ def get_verification_code(
match = re.search(pattern, re.sub(email_pattern, "", content))
if match:
code = match.group(1)
if not self._accept_verification_code(email, code, message_marker):
continue
logger.info(f"从自定义域名邮箱 {email} 找到验证码: {code}")
self.update_status(True)
return code
Expand Down
21 changes: 20 additions & 1 deletion src/services/temp_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,12 +335,29 @@ def get_verification_code(
time.sleep(3)
continue

for mail in mails:
ordered_mails = self._sort_items_by_message_time(
mails,
lambda item: (
item.get("createdAt")
or item.get("created_at")
or item.get("receivedAt")
or item.get("received_at")
) if isinstance(item, dict) else None,
)

for mail in ordered_mails:
mail_id = mail.get("id")
if not mail_id or mail_id in seen_mail_ids:
continue

seen_mail_ids.add(mail_id)
message_marker = f"id:{mail_id}"

if self._is_message_before_otp(
mail.get("createdAt") or mail.get("created_at") or mail.get("receivedAt") or mail.get("received_at"),
otp_sent_at,
):
continue

parsed = self._extract_mail_fields(mail)
sender = parsed["sender"].lower()
Expand All @@ -355,6 +372,8 @@ def get_verification_code(

code = self._extract_otp_from_text(content, pattern)
if code:
if not self._accept_verification_code(email, code, message_marker):
continue
logger.info(f"从 TempMail 邮箱 {email} 找到验证码: {code}")
self.update_status(True)
return code
Expand Down
10 changes: 9 additions & 1 deletion src/services/tempmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,12 @@ def get_verification_code(
time.sleep(3)
continue

for msg in email_list:
ordered_emails = self._sort_items_by_message_time(
email_list,
lambda item: item.get("date") if isinstance(item, dict) else None,
)

for msg in ordered_emails:
if not isinstance(msg, dict):
continue

Expand All @@ -260,6 +265,7 @@ def get_verification_code(
if not message_id or message_id in seen_ids:
continue
seen_ids.add(message_id)
message_marker = f"id:{message_id}"

sender = str(msg.get("from", "")).lower()
subject = str(msg.get("subject", ""))
Expand All @@ -276,6 +282,8 @@ def get_verification_code(
match = re.search(pattern, content)
if match:
code = match.group(1)
if not self._accept_verification_code(email, code, message_marker):
continue
logger.info(f"找到验证码: {code}")
self.update_status(True)
return code
Expand Down
Loading
Loading