"""결과 이메일 발송. 1순위: Azure Communication Services(ACS) Email (endpoint + accesskey). 2순위(폴백): SMTP (aiosmtplib). 둘 다 미설정이면 EmailUnavailable 을 던진다(조용한 실패 0). 경기 종료 후 워커가 구독자(notify=True + email)에게 개인화 결과 메일 발송. """ from __future__ import annotations import logging from email.message import EmailMessage from ..config import settings log = logging.getLogger("triplepick.email") class EmailUnavailable(RuntimeError): pass async def _send_acs(to: str, subject: str, html: str, text: str) -> None: """Azure Communication Services Email — 키(accesskey) 인증.""" from azure.communication.email.aio import EmailClient conn = f"endpoint={settings.azure_acs_endpoint};accesskey={settings.azure_acs_accesskey}" message = { "senderAddress": settings.azure_acs_sender, "recipients": {"to": [{"address": to}]}, "content": {"subject": subject, "plainText": text, "html": html}, } async with EmailClient.from_connection_string(conn) as client: poller = await client.begin_send(message) await poller.result() log.info("email sent via ACS → %s (%s)", to, subject) async def _send_smtp(to: str, subject: str, html: str, text: str) -> None: import aiosmtplib msg = EmailMessage() msg["From"] = settings.smtp_from msg["To"] = to msg["Subject"] = subject msg.set_content(text) msg.add_alternative(html, subtype="html") await aiosmtplib.send( msg, hostname=settings.smtp_host, port=settings.smtp_port, username=settings.smtp_user or None, password=settings.smtp_password or None, start_tls=settings.smtp_starttls, ) log.info("email sent via SMTP → %s (%s)", to, subject) async def send_email(to: str, subject: str, html: str, text: str) -> None: if settings.acs_configured: await _send_acs(to, subject, html, text) return if settings.smtp_host: await _send_smtp(to, subject, html, text) return # ACS endpoint/accesskey 만 있고 sender 누락 시 명확히 안내 if settings.azure_acs_endpoint and not settings.azure_acs_sender: raise EmailUnavailable("AZURE_ACS_SENDER(검증된 MailFrom 주소) 미설정") raise EmailUnavailable("이메일 미설정 — ACS(endpoint+accesskey+sender) 또는 SMTP 필요") def build_result_email( *, team_a: str, team_b: str, result_a: int, result_b: int, my_a: int, my_b: int, my_points: int, ai_lines: list[tuple[str, bool]], # (모델명, 적중여부) match_url: str, ) -> tuple[str, str, str]: """제목/HTML/텍스트 반환.""" hit = my_points > 0 headline = "적중! 🎯" if hit else "아쉽네요" subject = f"[TriplePick] {team_a} {result_a}-{result_b} {team_b} 결과 — {headline}" ai_html = "".join( f"
{team_a} {result_a} - {result_b} {team_b}
당신의 예측: {team_a} {my_a} - {my_b} {team_b} → {my_points}점 ({headline})
100만원 챌린지 — 누적 포인트 1위에게 최종 상금.