import html import logging from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText import aiosmtplib from app.engine.config import settings from app.engine.services.cc_completion import cc_state_for, outbound_e164, outbound_number logger = logging.getLogger(__name__) TAG_LABELS = { "interested": "意向强", "pending": "待确认", "not_interested": "无意向", "": "未评估", } CONTACT_LABELS = {"phone": "手机", "whatsapp": "WhatsApp", "email": "邮箱", "wechat": "微信"} # 平台默认发件人显示名(Task 9 前的硬编码值, 租户未配 sender_name 时沿用) _DEFAULT_SENDER_NAME = "HESUPIPE" def resolve_email_settings(config_data: dict | None) -> dict: """租户 config.email → 发信参数(Task 9 接线; env settings 回退)。 回退规则(语义钉在 test_config_merge: 显式 None = 清空/未设置): - smtp 缺省或显式 None → 整段用 env SMTP_*; - smtp 给了 dict → 段内逐键回退(缺键/空串 → env 对应值, 租户可只改 密码/只换 host); - sender_name 空 → 平台默认(_DEFAULT_SENDER_NAME); - notify_emails 空列表 → env NOTIFY_EMAIL 单元素。 """ email = ((config_data or {}).get("email")) or {} smtp = email.get("smtp") or {} notify = [e for e in (email.get("notify_emails") or []) if e] return { "host": smtp.get("host") or settings.SMTP_HOST, "port": smtp.get("port") or settings.SMTP_PORT, "user": smtp.get("user") or settings.SMTP_USER, "password": smtp.get("password") or settings.SMTP_PASS, "sender_name": email.get("sender_name") or _DEFAULT_SENDER_NAME, "notify_emails": notify or ( [settings.NOTIFY_EMAIL] if settings.NOTIFY_EMAIL else []), } def _esc(v) -> str: return html.escape(str(v)) if v else "" def _flat(v) -> str: """主题行用:折叠空白(堵换行注入头部的通道)+ 限长。""" return " ".join(str(v).split())[:60] def _contact_rows(contacts: dict) -> str: return "".join( f"
{_esc(reasoning)}
注:邮件为触发时快照,对话可能仍在进行,最新记录请以后台为准。
""" msg.attach(MIMEText(html_body, "html", "utf-8")) return await _send(msg, cfg) async def send_returning_lead_email( contact_info: dict, cc_info: dict | None = None, config_data: dict | None = None, ) -> bool: """D5: 24h 内同联系方式再访,只发一行轻提醒。主题行号码走 e164(Task 6)。""" _body, subj_contacts = _cc_views(contact_info, cc_info) contact_parts = [ f"{CONTACT_LABELS.get(k, k)}: {_flat(v)}" for k, v in subj_contacts.items() ] cfg = resolve_email_settings(config_data) msg = MIMEMultipart("alternative") msg["Subject"] = f"[线索回访] {' | '.join(contact_parts) or '已知客户'}再次来访" msg["From"] = f"【{cfg['sender_name']}】 <{cfg['user']}>" msg["To"] = ", ".join(cfg["notify_emails"]) msg.attach(MIMEText( f"该联系方式 24h 内已发过完整询盘,客户又开启了新对话," f"详情请查后台。
", "html", "utf-8")) return await _send(msg, cfg) async def send_correction_email( contact_info: dict, count: int = 1, cc_info: dict | None = None, config_data: dict | None = None, ) -> bool: """D4: 联系方式变化(改号/新增/无提示换值)后的轻量更正邮件,每会话上限3封。 正文联系方式行渲染补全号+标注(Task 6, spec §5 更正路径)。""" body_contacts, _subj = _cc_views(contact_info, cc_info) cfg = resolve_email_settings(config_data) msg = MIMEMultipart("alternative") msg["Subject"] = f"[询盘·更正{count}] 客户联系方式已更新(以本封为准)" msg["From"] = f"【{cfg['sender_name']}】 <{cfg['user']}>" msg["To"] = ", ".join(cfg["notify_emails"]) msg.attach(MIMEText( f"这是本会话第 {count} 次更正(上限 3 次),请以本封为准:
" f"