여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다. - ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제 - 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다 - eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다 파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이). site·frontend·admin tsc, site vitest 105 passed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
131 lines
3.9 KiB
Python
131 lines
3.9 KiB
Python
"""메일 한 통을 보낸다 — Azure Communication Services 우선, SMTP 폴백."""
|
|
import os
|
|
import re
|
|
import smtplib
|
|
import ssl
|
|
from email.message import EmailMessage
|
|
from email.utils import formataddr, parseaddr
|
|
|
|
from common.logger import LOG
|
|
|
|
ACS_ENDPOINT_ENV = "ACS_EMAIL_ENDPOINT"
|
|
ACS_KEY_ENV = "ACS_EMAIL_ACCESSKEY"
|
|
ACS_SENDER_ENV = "ACS_EMAIL_SENDER"
|
|
|
|
HOST_ENV = "SMTP_HOST"
|
|
PORT_ENV = "SMTP_PORT"
|
|
USER_ENV = "SMTP_USER"
|
|
PASSWORD_ENV = "SMTP_PASSWORD"
|
|
FROM_ENV = "SMTP_FROM"
|
|
FROM_NAME_ENV = "SMTP_FROM_NAME"
|
|
TLS_ENV = "SMTP_TLS"
|
|
|
|
TIMEOUT_SEC = 15.0
|
|
DEFAULT_FROM_NAME = "Web4AI"
|
|
|
|
|
|
def is_configured() -> bool:
|
|
return _acs_configured() or _smtp_configured()
|
|
|
|
|
|
def _acs_configured() -> bool:
|
|
return bool(_env(ACS_ENDPOINT_ENV) and _env(ACS_KEY_ENV) and _env(ACS_SENDER_ENV))
|
|
|
|
|
|
def _smtp_configured() -> bool:
|
|
return bool(_env(HOST_ENV) and _sender())
|
|
|
|
|
|
def _env(name: str, default: str = "") -> str:
|
|
return os.environ.get(name, default).strip()
|
|
|
|
|
|
def _sender() -> str:
|
|
return _env(FROM_ENV) or _env(USER_ENV)
|
|
|
|
|
|
def _port() -> int:
|
|
try:
|
|
return int(_env(PORT_ENV) or "587")
|
|
except ValueError:
|
|
return 587
|
|
|
|
|
|
def _mode() -> str:
|
|
"""starttls | ssl | plain."""
|
|
value = _env(TLS_ENV).lower()
|
|
if value in {"ssl", "starttls", "plain"}:
|
|
return value
|
|
return "ssl" if _port() == 465 else "starttls"
|
|
|
|
|
|
def is_valid_address(value: str) -> bool:
|
|
address = parseaddr(value or "")[1]
|
|
return bool(re.fullmatch(r"[^@\s]+@[^@\s.]+(\.[^@\s.]+)+", address))
|
|
|
|
|
|
def _send_acs(*, to: str, subject: str, text: str, reply_to: str | None) -> bool:
|
|
try:
|
|
from azure.communication.email import EmailClient
|
|
|
|
client = EmailClient.from_connection_string(
|
|
f"endpoint={_env(ACS_ENDPOINT_ENV)};accesskey={_env(ACS_KEY_ENV)}"
|
|
)
|
|
message = {
|
|
"senderAddress": _env(ACS_SENDER_ENV),
|
|
"recipients": {"to": [{"address": to}]},
|
|
"content": {"subject": subject, "plainText": text},
|
|
}
|
|
if reply_to:
|
|
message["replyTo"] = [{"address": reply_to}]
|
|
client.begin_send(message).result()
|
|
return True
|
|
except Exception as error: # noqa: BLE001 — 폴백(SMTP)이 남아 있다
|
|
LOG.e(f"[mail] ACS 발송 실패: {type(error).__name__}")
|
|
return False
|
|
|
|
|
|
def send(*, to: str, subject: str, text: str, reply_to: str | None = None) -> bool:
|
|
"""보냈으면 True."""
|
|
if not is_configured():
|
|
LOG.w("[mail] SMTP 미설정 — 보내지 않는다")
|
|
return False
|
|
if not is_valid_address(to):
|
|
LOG.w("[mail] 받는 주소가 형식에 맞지 않는다")
|
|
return False
|
|
|
|
if _acs_configured():
|
|
if _send_acs(to=to, subject=subject, text=text, reply_to=reply_to):
|
|
return True
|
|
if not _smtp_configured():
|
|
return False
|
|
|
|
if not _smtp_configured():
|
|
return False
|
|
|
|
message = EmailMessage()
|
|
message["Subject"] = subject
|
|
message["From"] = formataddr((_env(FROM_NAME_ENV) or DEFAULT_FROM_NAME, _sender()))
|
|
message["To"] = to
|
|
if reply_to and is_valid_address(reply_to):
|
|
message["Reply-To"] = reply_to
|
|
message.set_content(text)
|
|
|
|
host, port, mode = _env(HOST_ENV), _port(), _mode()
|
|
user, password = _env(USER_ENV), _env(PASSWORD_ENV)
|
|
try:
|
|
if mode == "ssl":
|
|
client = smtplib.SMTP_SSL(host, port, timeout=TIMEOUT_SEC, context=ssl.create_default_context())
|
|
else:
|
|
client = smtplib.SMTP(host, port, timeout=TIMEOUT_SEC)
|
|
with client:
|
|
if mode == "starttls":
|
|
client.starttls(context=ssl.create_default_context())
|
|
if user and password:
|
|
client.login(user, password)
|
|
client.send_message(message)
|
|
return True
|
|
except Exception as error: # noqa: BLE001 — 발송 실패를 호출측 실패로 만들지 않는다
|
|
LOG.e(f"[mail] 발송 실패: {type(error).__name__}")
|
|
return False
|