65 lines
2.3 KiB
Python
65 lines
2.3 KiB
Python
"""표준 라이브러리 기반 비동기 SMTP 발송 서비스."""
|
|
|
|
import asyncio
|
|
import logging
|
|
import smtplib
|
|
from email.message import EmailMessage
|
|
from email.utils import formataddr
|
|
|
|
from config.config_system import (
|
|
EMAIL_FROM_ADDRESS,
|
|
EMAIL_FROM_NAME,
|
|
EMAIL_NOTIFICATION_ENABLED,
|
|
EMAIL_RETRY_COUNT,
|
|
EMAIL_RETRY_DELAY_SECONDS,
|
|
SMTP_HOST,
|
|
SMTP_PASSWORD,
|
|
SMTP_PORT,
|
|
SMTP_USE_TLS,
|
|
SMTP_USERNAME,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _send_sync(to_email: str, subject: str, html: str) -> None:
|
|
message = EmailMessage()
|
|
message["From"] = formataddr((EMAIL_FROM_NAME, EMAIL_FROM_ADDRESS))
|
|
message["To"] = to_email
|
|
message["Subject"] = subject
|
|
message.set_content("HTML 이메일을 지원하는 메일 앱에서 확인해 주세요.")
|
|
message.add_alternative(html, subtype="html")
|
|
|
|
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=30) as smtp:
|
|
if SMTP_USE_TLS:
|
|
smtp.starttls()
|
|
smtp.login(SMTP_USERNAME, SMTP_PASSWORD)
|
|
smtp.send_message(message)
|
|
|
|
|
|
async def send_email(to_email: str, subject: str, html: str) -> bool:
|
|
"""SMTP 발송을 워커 스레드에서 실행하고 설정된 횟수만큼 재시도한다."""
|
|
if not EMAIL_NOTIFICATION_ENABLED:
|
|
logger.info("이메일 발송 비활성화: %s", to_email)
|
|
return True
|
|
if not SMTP_USERNAME or not SMTP_PASSWORD or not EMAIL_FROM_ADDRESS:
|
|
logger.error("SMTP 환경 변수가 설정되지 않았습니다.")
|
|
return False
|
|
|
|
for attempt in range(1, EMAIL_RETRY_COUNT + 1):
|
|
try:
|
|
await asyncio.to_thread(_send_sync, to_email, subject, html)
|
|
logger.info("이메일 발송 성공: %s", to_email)
|
|
return True
|
|
except (OSError, smtplib.SMTPException):
|
|
logger.exception("이메일 발송 실패 (%s/%s)", attempt, EMAIL_RETRY_COUNT)
|
|
if attempt < EMAIL_RETRY_COUNT:
|
|
await asyncio.sleep(EMAIL_RETRY_DELAY_SECONDS * (2 ** (attempt - 1)))
|
|
return False
|
|
|
|
|
|
def send_email_background(to_email: str, subject: str, html: str) -> None:
|
|
"""HTTP 응답을 차단하지 않고 이메일 발송을 예약한다."""
|
|
task = asyncio.create_task(send_email(to_email, subject, html))
|
|
task.add_done_callback(lambda completed: completed.exception())
|