"""SMTP 메일 발송 유틸 (표준 라이브러리 smtplib 만 사용 — 추가 의존성 없음). 모든 모듈이 공용으로 쓰는 가벼운 발송기. 현재는 프로젝트 관리 모듈의 관리자 알림(업무 배정/완료)에 사용한다. 환경변수: SMTP_HOST SMTP 서버 호스트 (미설정 시 발송 비활성 — 조용히 skip) SMTP_PORT 포트 (기본 587) SMTP_USER 로그인 사용자 (없으면 익명) SMTP_PASSWORD 로그인 비밀번호 SMTP_FROM 보내는 사람 (미설정 시 SMTP_USER) SMTP_TLS "true"(STARTTLS, 기본) | "ssl"(SMTPS/465) | "false"(평문) SMTP_TIMEOUT 연결 타임아웃 초 (기본 10) 설계 원칙: - 미설정/실패해도 앱이 죽지 않는다. 로그만 남기고 False 반환. - 호출부는 FastAPI BackgroundTasks 로 비동기 호출 권장(요청 지연 방지). """ from __future__ import annotations import logging import os import smtplib from email.message import EmailMessage from email.utils import formataddr logger = logging.getLogger("app.mail") def _env(name: str, default: str = "") -> str: return (os.getenv(name, "") or "").strip() or default def mail_enabled() -> bool: """SMTP_HOST 가 있으면 발송 가능으로 본다.""" return bool(_env("SMTP_HOST")) def send_email( *, to: list[str] | str, subject: str, body: str, html: str | None = None, from_name: str = "DBX ERP", ) -> bool: """단순 텍스트(+선택 HTML) 메일 1건 발송. 반환: 성공 True / 비활성·실패 False. 예외를 밖으로 던지지 않는다. """ host = _env("SMTP_HOST") if not host: logger.info("SMTP 미설정 — 메일 발송 skip (subject=%s)", subject) return False recipients = [to] if isinstance(to, str) else list(to) recipients = [r.strip() for r in recipients if r and r.strip()] if not recipients: logger.info("수신자 없음 — 메일 발송 skip (subject=%s)", subject) return False port = int(_env("SMTP_PORT", "587")) user = _env("SMTP_USER") password = _env("SMTP_PASSWORD") sender = _env("SMTP_FROM", user or "no-reply@dbxcorp.co.kr") mode = _env("SMTP_TLS", "true").lower() timeout = int(_env("SMTP_TIMEOUT", "10")) msg = EmailMessage() msg["Subject"] = subject msg["From"] = formataddr((from_name, sender)) msg["To"] = ", ".join(recipients) msg.set_content(body) if html: msg.add_alternative(html, subtype="html") try: if mode == "ssl": with smtplib.SMTP_SSL(host, port, timeout=timeout) as server: if user: server.login(user, password) server.send_message(msg) else: with smtplib.SMTP(host, port, timeout=timeout) as server: server.ehlo() if mode != "false": server.starttls() server.ehlo() if user: server.login(user, password) server.send_message(msg) logger.info("메일 발송 완료 → %s (subject=%s)", recipients, subject) return True except Exception as exc: # noqa: BLE001 — 메일 실패가 앱을 막으면 안 됨 logger.warning("메일 발송 실패 (subject=%s): %s", subject, exc) return False