Files
gsfdc/server/app/services/email_code.py
T

80 lines
2.7 KiB
Python

import re
import secrets
import string
from app.services.mailer import send_email
from app.services.redis_client import get_redis
EMAIL_CODE_TTL_SEC = 600
EMAIL_CODE_PURPOSES = ("register", "login", "reset")
_EMAIL_RE = re.compile(r"^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$")
def normalize_email(email: str) -> str:
return (email or "").strip().lower()
def is_valid_email(email: str) -> bool:
text = normalize_email(email)
return bool(text) and len(text) <= 191 and _EMAIL_RE.match(text) is not None
def _key(purpose: str, email: str) -> str:
return f"gsfdc:email_code:{purpose}:{normalize_email(email)}"
def _rate_key(purpose: str, email: str) -> str:
return f"gsfdc:email_code_rate:{purpose}:{normalize_email(email)}"
def _new_code() -> str:
return "".join(secrets.choice(string.digits) for _ in range(6))
def issue_email_code(*, email: str, purpose: str) -> None:
if purpose not in EMAIL_CODE_PURPOSES:
raise ValueError("验证码用途不正确")
if not is_valid_email(email):
raise ValueError("邮箱格式不正确")
normalized = normalize_email(email)
redis = get_redis()
if redis.get(_rate_key(purpose, normalized)):
raise ValueError("发送过于频繁,请稍后再试")
code = _new_code()
redis.set(_key(purpose, normalized), code, ex=EMAIL_CODE_TTL_SEC)
redis.set(_rate_key(purpose, normalized), "1", ex=60)
labels = {"register": "注册", "login": "登录", "reset": "重置密码"}
subject = f"甘肃省房地产价格评估行业协会 · {labels[purpose]}验证码"
body = (
f"您的{labels[purpose]}验证码是:{code}\n"
f"有效期 {EMAIL_CODE_TTL_SEC // 60} 分钟。如非本人操作请忽略本邮件。\n"
)
try:
send_email(to=normalized, subject=subject, body=body)
except Exception:
# 发信失败时仍保留验证码,便于本地/测试校验路径;生产日志可见失败。
import logging
logging.getLogger("gsfdc.email_code").exception("邮箱验证码发信失败")
def consume_email_code(*, email: str, purpose: str, code: str) -> bool:
if purpose not in EMAIL_CODE_PURPOSES:
return False
if not is_valid_email(email) or not isinstance(code, str):
return False
normalized = normalize_email(email)
text = code.strip()
if not text:
return False
# 仅在码匹配时删除;输错不消耗,避免一次错码废掉仍有效的验证码。
redis = get_redis()
key = _key(purpose, normalized)
deleted = redis.eval(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
1,
key,
text,
)
return bool(deleted)