feat(project): 아사나식 프로젝트 관리 모듈 추가
- 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>
This commit is contained in:
+97
@@ -0,0 +1,97 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user