64 lines
2.2 KiB
Python
64 lines
2.2 KiB
Python
import logging
|
|
import dns.resolver
|
|
from django.core.cache import caches
|
|
|
|
logger = logging.getLogger(__name__)
|
|
default_cache = caches['default']
|
|
|
|
MX_CACHE_TTL = 3600 # MX 验证结果缓存1小时,避免每次请求都做慢速DNS查询
|
|
|
|
# 常用邮箱域名白名单:大概率可正常收发,直接放行,无需DNS查询
|
|
COMMON_EMAIL_DOMAINS = {
|
|
'qq.com', 'foxmail.com', '163.com', '126.com', 'yeah.net',
|
|
'139.com', '189.cn', 'sina.com', 'sohu.com', '21cn.com',
|
|
'gmail.com', 'googlemail.com', 'outlook.com', 'hotmail.com',
|
|
'live.com', 'msn.com', 'icloud.com', 'me.com', 'yahoo.com',
|
|
'proton.me', 'protonmail.com', 'gmx.com', 'gmx.net', 'aol.com', 'zoho.com',
|
|
}
|
|
|
|
|
|
def validate_email_mx(email):
|
|
"""验证邮箱域名是否有MX记录(通过DNS MX查询),排除 null MX (MX 0 .) 的情况"""
|
|
try:
|
|
domain = email.split('@')[1].lower()
|
|
except IndexError:
|
|
return False
|
|
|
|
# 常用邮箱域名直接放行,免DNS查询
|
|
if domain in COMMON_EMAIL_DOMAINS:
|
|
return True
|
|
|
|
cache_key = f'mx_valid_{domain}'
|
|
try:
|
|
cached = default_cache.get(cache_key)
|
|
if cached is not None:
|
|
return cached
|
|
except Exception:
|
|
pass
|
|
|
|
valid = _query_mx(domain)
|
|
try:
|
|
default_cache.set(cache_key, valid, timeout=MX_CACHE_TTL)
|
|
except Exception:
|
|
pass
|
|
return valid
|
|
|
|
|
|
def _query_mx(domain):
|
|
try:
|
|
answers = dns.resolver.resolve(domain, 'MX')
|
|
# 检查是否是 null MX (MX 0 .) — 域名明确拒绝收件
|
|
for answer in answers:
|
|
exchange = answer.exchange.to_text().rstrip('.')
|
|
if exchange: # 有真实的邮件服务器
|
|
return True
|
|
# 所有 MX 记录都指向空(null MX),表示不接受邮件
|
|
logger.warning(f'[Email] Domain has null MX (rejects mail): {domain}')
|
|
return False
|
|
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.resolver.NoNameservers):
|
|
logger.warning(f'[Email] No MX record found for domain: {domain}')
|
|
return False
|
|
except Exception as e:
|
|
logger.warning(f'[Email] MX record check failed: domain={domain}, error={str(e)}')
|
|
return False
|