feat: 配置中心-凭据(邮件推送AI表单/掩码交互/SMTP测试/推送探测端点)
- 后端 POST /tenants/{slug}/email/test:resolve_email_settings 当前配置
真发测试邮件(主题 [all-kefu] SMTP 测试邮件+租户名+UTC+8 时间);
回退后 host/user 仍空 → 400;成败如实返回 {sent, error},密钥不进响应
- 后端 POST /tenants/{slug}/push/test:连通性探测——与 _do_post 同款
连接形态 POST 空 messages 批量(红线:绝不携带线索数据);不可达
reason 分类 dns/timeout/connect(Windows 裸 ConnectError 文案兜底),
auth_ok 三态(2xx 真 / 401·403 假 / 其余 null),单次无重试
- email_service 抽 _smtp_send 共用连接逻辑(_send 契约不变)+
send_test_email(成败+错误串如实返回,不走吞异常 bool)
- 前端 CredsPane:邮件/推送/AI 三组凭据表单(数值/邮箱前端预检 +
后端 400 双层);密钥哨兵交互(__SET__ 占位符/不动保存保持库值/
清除按钮显式清空可撤销);SMTP 测试与推送探测三态内联展示
- 测试:pytest 新增 18(email/test 7 + push/test 11),全量 595 通过;
vitest 新增 20(model 哨兵往返/预检边界 + API 调用形态),107 通过
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import html
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
@@ -58,16 +59,23 @@ def _cc_views(contacts: dict, cc_info: dict | None) -> tuple[dict, dict]:
|
||||
return body, subj
|
||||
|
||||
|
||||
async def _smtp_send(msg: MIMEMultipart, cfg: dict) -> None:
|
||||
"""底层连接发送(Phase 3 Task 7c 从 _send 抽出): 直调 aiosmtplib,
|
||||
失败抛原始异常——需要错误串的调用方(配置中心测试邮件 send_test_email)
|
||||
复用同一连接逻辑;_send 在此之上包"吞异常返 bool"契约,行为不变。"""
|
||||
await aiosmtplib.send(
|
||||
msg, hostname=cfg["host"], port=cfg["port"],
|
||||
username=cfg["user"], password=cfg["password"],
|
||||
use_tls=True,
|
||||
)
|
||||
|
||||
|
||||
async def _send(msg: MIMEMultipart, cfg: dict) -> bool:
|
||||
if not all([cfg["host"], cfg["user"], cfg["notify_emails"]]):
|
||||
logger.warning("SMTP not configured, skipping email")
|
||||
return False
|
||||
try:
|
||||
await aiosmtplib.send(
|
||||
msg, hostname=cfg["host"], port=cfg["port"],
|
||||
username=cfg["user"], password=cfg["password"],
|
||||
use_tls=True,
|
||||
)
|
||||
await _smtp_send(msg, cfg)
|
||||
logger.info("Lead email sent to %s", cfg["notify_emails"])
|
||||
return True
|
||||
except Exception:
|
||||
@@ -170,3 +178,35 @@ async def send_correction_email(
|
||||
f"<table>{_contact_rows(body_contacts)}</table>"
|
||||
f"</body></html>", "html", "utf-8"))
|
||||
return await _send(msg, cfg)
|
||||
|
||||
|
||||
async def send_test_email(
|
||||
to: str, tenant_name: str = "",
|
||||
config_data: dict | None = None,
|
||||
) -> tuple[bool, str | None]:
|
||||
"""配置中心测试邮件(Phase 3 Task 7c): 按当前生效邮件配置真发一封到
|
||||
指定收件人——主题/正文钉契约("[all-kefu] SMTP 测试邮件"+租户名+时间,
|
||||
时间与 lead_push_service 同款 +08:00 口径)。
|
||||
|
||||
与 _send 的吞异常 bool 契约解耦: 成败与错误串如实返回(探测端点直传
|
||||
前端,凭据对不对让收件箱与错误串说话);连接逻辑复用 _smtp_send。
|
||||
"SMTP 未配置"的 400 判定在端点层(本函数假定 cfg 已可用)。"""
|
||||
cfg = resolve_email_settings(config_data)
|
||||
now = datetime.now(timezone(timedelta(hours=8)))
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = "[all-kefu] SMTP 测试邮件"
|
||||
msg["From"] = f"【{cfg['sender_name']}】 <{cfg['user']}>"
|
||||
msg["To"] = to
|
||||
msg.attach(MIMEText(
|
||||
f"<html><body>"
|
||||
f"<p>这是一封 SMTP 配置测试邮件——收到本邮件说明该租户的邮件配置"
|
||||
f"可以正常发信。</p>"
|
||||
f"<p>租户:{_esc(tenant_name)}</p>"
|
||||
f"<p>时间:{now.strftime('%Y-%m-%d %H:%M:%S')}(UTC+8)</p>"
|
||||
f"</body></html>", "html", "utf-8"))
|
||||
try:
|
||||
await _smtp_send(msg, cfg)
|
||||
return True, None
|
||||
except Exception as exc:
|
||||
logger.exception("Test email to %s failed", to)
|
||||
return False, repr(exc)
|
||||
|
||||
Reference in New Issue
Block a user