From 7185cfbb72d27012f3335fe88e866a5ad035cde2 Mon Sep 17 00:00:00 2001 From: hbyang Date: Thu, 11 Jun 2026 09:21:10 +0900 Subject: [PATCH] =?UTF-8?q?mail=20config=20=EC=A0=81=EC=9A=A9=20.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env.example | 9 +++++++- backend/app/config.py | 14 +++++++++++- backend/app/services/email.py | 41 ++++++++++++++++++++++++++++------- backend/requirements.txt | 2 ++ 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 7bbc8c7..dc32d54 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -50,7 +50,14 @@ ANTHROPIC_MODEL=claude-opus-4-8 GOOGLE_API_KEY= GOOGLE_MODEL=gemini-2.5-flash -# ── 외부 연동: 이메일 (SMTP, 실연동) ── +# ── 외부 연동: 이메일 ── +# 1순위: Azure Communication Services(ACS) Email (endpoint + accesskey) +# AZURE_ACS_SENDER 는 검증된 MailFrom 주소(예: donotreply@triplepick.o2o.kr) +AZURE_ACS_ENDPOINT=https://o2o-common-acs.korea.communication.azure.com/ +AZURE_ACS_ACCESSKEY= +AZURE_ACS_SENDER=donotreply@triplepick.o2o.kr + +# 2순위(폴백): SMTP — ACS 미설정 시 사용 SMTP_HOST= SMTP_PORT=587 SMTP_USER= diff --git a/backend/app/config.py b/backend/app/config.py index 50f3bb5..b53a87c 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -89,7 +89,13 @@ class Settings(BaseSettings): google_api_key: str = "" google_model: str = "gemini-2.5-flash" - # ── 외부 연동: 이메일 (SMTP, 실연동) ───────────────────── + # ── 외부 연동: 이메일 ───────────────────────────────────── + # 1순위: Azure Communication Services(ACS) Email — endpoint + accesskey. + # AZURE_ACS_SENDER 는 검증된 MailFrom 주소(예: donotreply@triplepick.o2o.kr). + azure_acs_endpoint: str = "" + azure_acs_accesskey: str = "" + azure_acs_sender: str = "" + # 2순위(폴백): SMTP — ACS 미설정 시 사용. smtp_host: str = "" smtp_port: int = 587 smtp_user: str = "" @@ -97,6 +103,12 @@ class Settings(BaseSettings): smtp_from: str = "TriplePick " smtp_starttls: bool = True + @property + def acs_configured(self) -> bool: + return bool( + self.azure_acs_endpoint and self.azure_acs_accesskey and self.azure_acs_sender + ) + @property def cors_origin_list(self) -> list[str]: if self.cors_origins.strip() == "*": diff --git a/backend/app/services/email.py b/backend/app/services/email.py index d952bdb..6338d49 100644 --- a/backend/app/services/email.py +++ b/backend/app/services/email.py @@ -1,6 +1,8 @@ -"""결과 이메일 발송 — SMTP 실연동 (aiosmtplib). +"""결과 이메일 발송. -SMTP 설정이 없으면 EmailUnavailable 을 던진다(조용한 실패 0). +1순위: Azure Communication Services(ACS) Email (endpoint + accesskey). +2순위(폴백): SMTP (aiosmtplib). +둘 다 미설정이면 EmailUnavailable 을 던진다(조용한 실패 0). 경기 종료 후 워커가 구독자(notify=True + email)에게 개인화 결과 메일 발송. """ from __future__ import annotations @@ -17,13 +19,23 @@ class EmailUnavailable(RuntimeError): pass -def _ensure_configured() -> None: - if not settings.smtp_host: - raise EmailUnavailable("SMTP_HOST 미설정") +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_email(to: str, subject: str, html: str, text: str) -> None: - _ensure_configured() +async def _send_smtp(to: str, subject: str, html: str, text: str) -> None: import aiosmtplib msg = EmailMessage() @@ -41,7 +53,20 @@ async def send_email(to: str, subject: str, html: str, text: str) -> None: password=settings.smtp_password or None, start_tls=settings.smtp_starttls, ) - log.info("email sent → %s (%s)", to, subject) + 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( diff --git a/backend/requirements.txt b/backend/requirements.txt index de83bd1..e43ee4a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,6 +7,8 @@ pydantic-settings==2.7.1 email-validator==2.2.0 apscheduler==3.11.0 aiosmtplib==3.0.2 +azure-communication-email==1.0.0 +aiohttp==3.10.11 httpx==0.27.2 anthropic==0.69.0 openai==1.59.6