feat(email): update email polling logic to use configurable intervals

This commit is contained in:
cnlimiter
2026-03-28 00:48:16 +08:00
parent 101169ec4a
commit b751d656ea
12 changed files with 46 additions and 53 deletions

View File

@@ -37,7 +37,7 @@ from ..config.settings import get_settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
OTP_SECONDARY_TIMEOUT_SECONDS = 120 OTP_SECONDARY_TIMEOUT_SECONDS = 120 # 默认值,运行时由 get_settings().email_code_timeout 覆盖
PHASE_EMAIL_PREPARE = "email_prepare" PHASE_EMAIL_PREPARE = "email_prepare"
PHASE_OTP_SECONDARY = "otp_secondary" PHASE_OTP_SECONDARY = "otp_secondary"
ERROR_EMAIL_PROVIDER_RATE_LIMITED = "EMAIL_PROVIDER_RATE_LIMITED" ERROR_EMAIL_PROVIDER_RATE_LIMITED = "EMAIL_PROVIDER_RATE_LIMITED"
@@ -646,7 +646,7 @@ class RegistrationEngine:
email_id = self.email_info.get("service_id") if self.email_info else None email_id = self.email_info.get("service_id") if self.email_info else None
budget = Budget( budget = Budget(
timeout_seconds=OTP_SECONDARY_TIMEOUT_SECONDS, timeout_seconds=get_settings().email_code_timeout,
started_at=started_at if started_at is not None else time.time(), started_at=started_at if started_at is not None else time.time(),
) )
remaining_timeout = budget.remaining_seconds() remaining_timeout = budget.remaining_seconds()

View File

@@ -13,11 +13,21 @@ from typing import Optional, Dict, Any, List
from enum import Enum from enum import Enum
from ..config.constants import EmailServiceType, OTP_CODE_PATTERN, OTP_CODE_SEMANTIC_PATTERN from ..config.constants import EmailServiceType, OTP_CODE_PATTERN, OTP_CODE_SEMANTIC_PATTERN
from ..config.settings import get_settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
EMAIL_PROVIDER_BACKOFF_BASE_SECONDS = 30 EMAIL_PROVIDER_BACKOFF_BASE_SECONDS = 30
def get_email_code_settings() -> dict:
"""获取验证码等待配置timeout、poll_interval"""
settings = get_settings()
return {
"timeout": settings.email_code_timeout,
"poll_interval": settings.email_code_poll_interval,
}
EMAIL_PROVIDER_BACKOFF_MAX_SECONDS = 3600 EMAIL_PROVIDER_BACKOFF_MAX_SECONDS = 3600
OTP_TIMEOUT_ERROR_PREFIX = "OTP_TIMEOUT" OTP_TIMEOUT_ERROR_PREFIX = "OTP_TIMEOUT"

View File

@@ -10,7 +10,7 @@ import time
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from .base import BaseEmailService, EmailServiceError, EmailServiceType, RateLimitedEmailServiceError from .base import BaseEmailService, EmailServiceError, EmailServiceType, RateLimitedEmailServiceError, get_email_code_settings
from ..config.constants import OTP_CODE_PATTERN from ..config.constants import OTP_CODE_PATTERN
from ..core.http_client import HTTPClient, RequestConfig from ..core.http_client import HTTPClient, RequestConfig
@@ -231,6 +231,7 @@ class CloudMailService(BaseEmailService):
) -> Optional[str]: ) -> Optional[str]:
logger.info(f"正在从 Cloud Mail 邮箱 {email} 获取验证码...") logger.info(f"正在从 Cloud Mail 邮箱 {email} 获取验证码...")
poll_interval = get_email_code_settings()["poll_interval"]
start_time = time.time() start_time = time.time()
seen_mail_ids: set = set() seen_mail_ids: set = set()
@@ -252,7 +253,7 @@ class CloudMailService(BaseEmailService):
mails = mails["list"] mails = mails["list"]
if not isinstance(mails, list): if not isinstance(mails, list):
time.sleep(3) time.sleep(poll_interval)
continue continue
for mail in mails: for mail in mails:
@@ -288,7 +289,7 @@ class CloudMailService(BaseEmailService):
except Exception as e: except Exception as e:
logger.debug(f"检查 Cloud Mail 邮件时出错: {e}") logger.debug(f"检查 Cloud Mail 邮件时出错: {e}")
time.sleep(3) time.sleep(poll_interval)
logger.warning(f"等待 Cloud Mail 验证码超时: {email}") logger.warning(f"等待 Cloud Mail 验证码超时: {email}")
return None return None

View File

@@ -12,7 +12,7 @@ from datetime import datetime, timezone
from html import unescape from html import unescape
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from .base import BaseEmailService, EmailServiceError, EmailServiceType, RateLimitedEmailServiceError from .base import BaseEmailService, EmailServiceError, EmailServiceType, RateLimitedEmailServiceError, get_email_code_settings
from ..config.constants import OTP_CODE_PATTERN from ..config.constants import OTP_CODE_PATTERN
from ..core.http_client import HTTPClient, RequestConfig from ..core.http_client import HTTPClient, RequestConfig
@@ -258,6 +258,7 @@ class DuckMailService(BaseEmailService):
logger.warning(f"DuckMail 邮箱缺少访问 token: {email}") logger.warning(f"DuckMail 邮箱缺少访问 token: {email}")
return None return None
poll_interval = get_email_code_settings()["poll_interval"]
start_time = time.time() start_time = time.time()
seen_message_ids = set() seen_message_ids = set()
@@ -307,7 +308,7 @@ class DuckMailService(BaseEmailService):
except Exception as e: except Exception as e:
logger.debug(f"DuckMail 轮询验证码失败: {e}") logger.debug(f"DuckMail 轮询验证码失败: {e}")
time.sleep(3) time.sleep(poll_interval)
return None return None

View File

@@ -10,7 +10,7 @@ import random
import string import string
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from .base import BaseEmailService, EmailServiceError, EmailServiceType, RateLimitedEmailServiceError from .base import BaseEmailService, EmailServiceError, EmailServiceType, RateLimitedEmailServiceError, get_email_code_settings
from ..core.http_client import HTTPClient, RequestConfig from ..core.http_client import HTTPClient, RequestConfig
from ..config.constants import OTP_CODE_PATTERN from ..config.constants import OTP_CODE_PATTERN
@@ -211,6 +211,7 @@ class FreemailService(BaseEmailService):
""" """
logger.info(f"正在从 Freemail 邮箱 {email} 获取验证码...") logger.info(f"正在从 Freemail 邮箱 {email} 获取验证码...")
poll_interval = get_email_code_settings()["poll_interval"]
start_time = time.time() start_time = time.time()
seen_mail_ids: set = set() seen_mail_ids: set = set()
@@ -218,7 +219,7 @@ class FreemailService(BaseEmailService):
try: try:
mails = self._make_request("GET", "/api/emails", params={"mailbox": email, "limit": 20}) mails = self._make_request("GET", "/api/emails", params={"mailbox": email, "limit": 20})
if not isinstance(mails, list): if not isinstance(mails, list):
time.sleep(3) time.sleep(poll_interval)
continue continue
ordered_mails = self._sort_items_by_message_time( ordered_mails = self._sort_items_by_message_time(
@@ -287,7 +288,7 @@ class FreemailService(BaseEmailService):
except Exception as e: except Exception as e:
logger.debug(f"检查 Freemail 邮件时出错: {e}") logger.debug(f"检查 Freemail 邮件时出错: {e}")
time.sleep(3) time.sleep(poll_interval)
logger.warning(f"等待 Freemail 验证码超时: {email}") logger.warning(f"等待 Freemail 验证码超时: {email}")
return None return None

View File

@@ -12,7 +12,7 @@ import logging
from email.header import decode_header from email.header import decode_header
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from .base import BaseEmailService, EmailServiceError from .base import BaseEmailService, EmailServiceError, get_email_code_settings
from ..config.constants import ( from ..config.constants import (
EmailServiceType, EmailServiceType,
OPENAI_EMAIL_SENDERS, OPENAI_EMAIL_SENDERS,
@@ -123,6 +123,7 @@ class ImapMailService(BaseEmailService):
otp_sent_at: Optional[float] = None, otp_sent_at: Optional[float] = None,
) -> Optional[str]: ) -> Optional[str]:
"""轮询 IMAP 收件箱,获取 OpenAI 验证码""" """轮询 IMAP 收件箱,获取 OpenAI 验证码"""
poll_interval = get_email_code_settings()["poll_interval"]
start_time = time.time() start_time = time.time()
seen_ids: set = set() seen_ids: set = set()
mail = None mail = None
@@ -136,7 +137,7 @@ class ImapMailService(BaseEmailService):
# 搜索所有未读邮件 # 搜索所有未读邮件
status, data = mail.search(None, "UNSEEN") status, data = mail.search(None, "UNSEEN")
if status != "OK" or not data or not data[0]: if status != "OK" or not data or not data[0]:
time.sleep(3) time.sleep(poll_interval)
continue continue
msg_ids = data[0].split() msg_ids = data[0].split()
@@ -177,7 +178,7 @@ class ImapMailService(BaseEmailService):
except Exception: except Exception:
pass pass
time.sleep(3) time.sleep(poll_interval)
except Exception as e: except Exception as e:
logger.warning(f"IMAP 连接/轮询失败: {e}") logger.warning(f"IMAP 连接/轮询失败: {e}")

View File

@@ -10,7 +10,7 @@ import logging
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from urllib.parse import urljoin from urllib.parse import urljoin
from .base import BaseEmailService, EmailServiceError, EmailServiceType, RateLimitedEmailServiceError from .base import BaseEmailService, EmailServiceError, EmailServiceType, RateLimitedEmailServiceError, get_email_code_settings
from ..core.http_client import HTTPClient, RequestConfig from ..core.http_client import HTTPClient, RequestConfig
from ..config.constants import OTP_CODE_PATTERN from ..config.constants import OTP_CODE_PATTERN
@@ -303,6 +303,7 @@ class MeoMailEmailService(BaseEmailService):
logger.info(f"正在从自定义域名邮箱 {email} 获取验证码...") logger.info(f"正在从自定义域名邮箱 {email} 获取验证码...")
poll_interval = get_email_code_settings()["poll_interval"]
start_time = time.time() start_time = time.time()
seen_message_ids = set() seen_message_ids = set()
@@ -313,7 +314,7 @@ class MeoMailEmailService(BaseEmailService):
messages = response.get("messages", []) messages = response.get("messages", [])
if not isinstance(messages, list): if not isinstance(messages, list):
time.sleep(3) time.sleep(poll_interval)
continue continue
ordered_messages = self._sort_items_by_message_time( ordered_messages = self._sort_items_by_message_time(
@@ -370,7 +371,7 @@ class MeoMailEmailService(BaseEmailService):
logger.debug(f"检查邮件时出错: {e}") logger.debug(f"检查邮件时出错: {e}")
# 等待一段时间再检查 # 等待一段时间再检查
time.sleep(3) time.sleep(poll_interval)
logger.warning(f"等待验证码超时: {email}") logger.warning(f"等待验证码超时: {email}")
return None return None

View File

@@ -8,9 +8,8 @@ import threading
import time import time
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from ..base import BaseEmailService, EmailServiceError, EmailServiceStatus, EmailServiceType from ..base import BaseEmailService, EmailServiceError, EmailServiceStatus, EmailServiceType, get_email_code_settings
from ...config.constants import EmailServiceType as ServiceType from ...config.constants import EmailServiceType as ServiceType
from ...config.settings import get_settings
from .account import OutlookAccount from .account import OutlookAccount
from .base import ProviderType, EmailMessage from .base import ProviderType, EmailMessage
from .email_parser import EmailParser, get_email_parser from .email_parser import EmailParser, get_email_parser
@@ -34,15 +33,6 @@ DEFAULT_PROVIDER_PRIORITY = [
] ]
def get_email_code_settings() -> dict:
"""获取验证码等待配置"""
settings = get_settings()
return {
"timeout": settings.email_code_timeout,
"poll_interval": settings.email_code_poll_interval,
}
class OutlookService(BaseEmailService): class OutlookService(BaseEmailService):
""" """
Outlook 邮箱服务 Outlook 邮箱服务

View File

@@ -20,7 +20,7 @@ from email.header import decode_header
from email.utils import parsedate_to_datetime from email.utils import parsedate_to_datetime
from urllib.error import HTTPError from urllib.error import HTTPError
from .base import BaseEmailService, EmailServiceError, EmailServiceType from .base import BaseEmailService, EmailServiceError, EmailServiceType, get_email_code_settings
from ..config.constants import ( from ..config.constants import (
OTP_CODE_PATTERN, OTP_CODE_PATTERN,
OTP_CODE_SIMPLE_PATTERN, OTP_CODE_SIMPLE_PATTERN,
@@ -28,21 +28,6 @@ from ..config.constants import (
OPENAI_EMAIL_SENDERS, OPENAI_EMAIL_SENDERS,
OPENAI_VERIFICATION_KEYWORDS, OPENAI_VERIFICATION_KEYWORDS,
) )
from ..config.settings import get_settings
def get_email_code_settings() -> dict:
"""
获取验证码等待配置
Returns:
dict: 包含 timeout 和 poll_interval 的字典
"""
settings = get_settings()
return {
"timeout": settings.email_code_timeout,
"poll_interval": settings.email_code_poll_interval,
}
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -15,7 +15,7 @@ from email.policy import default as email_policy
from html import unescape from html import unescape
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from .base import BaseEmailService, EmailServiceError, EmailServiceType, RateLimitedEmailServiceError from .base import BaseEmailService, EmailServiceError, EmailServiceType, RateLimitedEmailServiceError, get_email_code_settings
from ..core.http_client import HTTPClient, RequestConfig from ..core.http_client import HTTPClient, RequestConfig
from ..config.constants import OTP_CODE_PATTERN from ..config.constants import OTP_CODE_PATTERN
@@ -307,6 +307,7 @@ class TempMailService(BaseEmailService):
logger.info(f"正在从 TempMail 邮箱 {email} 获取验证码...") logger.info(f"正在从 TempMail 邮箱 {email} 获取验证码...")
start_time = time.time() start_time = time.time()
poll_interval = get_email_code_settings()["poll_interval"]
seen_mail_ids: set = set() seen_mail_ids: set = set()
# 优先使用用户级 JWT回退到 admin API 先注释用户级API # 优先使用用户级 JWT回退到 admin API 先注释用户级API
@@ -332,7 +333,7 @@ class TempMailService(BaseEmailService):
# /user_api/mails 和 /admin/mails 返回格式相同: {"results": [...], "total": N} # /user_api/mails 和 /admin/mails 返回格式相同: {"results": [...], "total": N}
mails = response.get("results", []) mails = response.get("results", [])
if not isinstance(mails, list): if not isinstance(mails, list):
time.sleep(3) time.sleep(poll_interval)
continue continue
ordered_mails = self._sort_items_by_message_time( ordered_mails = self._sort_items_by_message_time(
@@ -381,7 +382,7 @@ class TempMailService(BaseEmailService):
except Exception as e: except Exception as e:
logger.debug(f"检查 TempMail 邮件时出错: {e}") logger.debug(f"检查 TempMail 邮件时出错: {e}")
time.sleep(3) time.sleep(poll_interval)
logger.warning(f"等待 TempMail 验证码超时: {email}") logger.warning(f"等待 TempMail 验证码超时: {email}")
return None return None

View File

@@ -8,7 +8,7 @@ import logging
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from datetime import datetime, timezone from datetime import datetime, timezone
from .base import BaseEmailService, EmailServiceError, EmailServiceType from .base import BaseEmailService, EmailServiceError, EmailServiceType, get_email_code_settings
from ..core.http_client import HTTPClient, RequestConfig from ..core.http_client import HTTPClient, RequestConfig
from ..config.constants import OTP_CODE_PATTERN from ..config.constants import OTP_CODE_PATTERN
@@ -212,6 +212,7 @@ class TempmailService(BaseEmailService):
logger.info(f"正在等待邮箱 {email} 的验证码...") logger.info(f"正在等待邮箱 {email} 的验证码...")
poll_interval = get_email_code_settings()["poll_interval"]
start_time = time.time() start_time = time.time()
seen_ids = set() seen_ids = set()
@@ -225,7 +226,7 @@ class TempmailService(BaseEmailService):
) )
if response.status_code != 200: if response.status_code != 200:
time.sleep(3) time.sleep(poll_interval)
continue continue
data = response.json() data = response.json()
@@ -238,7 +239,7 @@ class TempmailService(BaseEmailService):
email_list = data.get("emails", []) if isinstance(data, dict) else [] email_list = data.get("emails", []) if isinstance(data, dict) else []
if not isinstance(email_list, list): if not isinstance(email_list, list):
time.sleep(3) time.sleep(poll_interval)
continue continue
ordered_emails = self._sort_items_by_message_time( ordered_emails = self._sort_items_by_message_time(
@@ -292,7 +293,7 @@ class TempmailService(BaseEmailService):
logger.debug(f"检查邮件时出错: {e}") logger.debug(f"检查邮件时出错: {e}")
# 等待一段时间再检查 # 等待一段时间再检查
time.sleep(3) time.sleep(poll_interval)
logger.warning(f"等待验证码超时: {email}") logger.warning(f"等待验证码超时: {email}")
return None return None
@@ -384,6 +385,7 @@ class TempmailService(BaseEmailService):
Returns: Returns:
验证码或 None 验证码或 None
""" """
poll_interval = get_email_code_settings()["poll_interval"]
start_time = time.time() start_time = time.time()
seen_ids = set() seen_ids = set()
check_count = 0 check_count = 0
@@ -402,7 +404,7 @@ class TempmailService(BaseEmailService):
try: try:
data = self.get_inbox(token) data = self.get_inbox(token)
if not data: if not data:
time.sleep(3) time.sleep(poll_interval)
continue continue
# 检查 inbox 是否过期 # 检查 inbox 是否过期
@@ -465,7 +467,7 @@ class TempmailService(BaseEmailService):
"message": "检查邮件时出错" "message": "检查邮件时出错"
}) })
time.sleep(3) time.sleep(poll_interval)
if callback: if callback:
callback({ callback({

View File

@@ -559,7 +559,7 @@ MyProxy|socks5://user:pass@host:port"></textarea>
<div class="card" style="margin-top: var(--spacing-lg);"> <div class="card" style="margin-top: var(--spacing-lg);">
<div class="card-header"> <div class="card-header">
<h3>验证码等待配置</h3> <h3>验证码等待配置</h3>
<span class="hint">配置 Outlook 邮箱验证码获取的超时时间和轮询间隔</span> <span class="hint">配置验证码获取的超时时间和轮询间隔</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<form id="email-code-form"> <form id="email-code-form">