c192d61c71
- project_db 신규(projects/members/stages/tasks/comments/activity, project_app CRUD) - 프로젝트/서브프로젝트(self-FK), 업무·진행단계(칸반), 멤버 배정 - 메인 뷰 달력(FullCalendar)/타임라인(vis-timeline) 토글 + 보드/리스트 - 진입은 로그인 회사 직원 전원, 생성/배정은 관리자 전용 - 업무 배정·완료 시 관리자 메일 알림(app/mail.py, SMTP_* env, 미설정 시 skip) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
3.3 KiB
Python
98 lines
3.3 KiB
Python
"""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
|