feat: Add email notifications for all notification types

Sends HTML email via Gmail SMTP when notifications are created.
Supports multi-language (en/ko/mn/ru) based on user country.
Runs in background thread to avoid blocking requests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AutonetSellCar Deploy
2026-02-18 09:55:12 +09:00
parent 436712367c
commit 46973c8508
2 changed files with 206 additions and 1 deletions

View File

@@ -10,6 +10,7 @@ from ..schemas.notification import (
NotificationCreate, NotificationResponse,
NotificationListResponse, NotificationMarkRead
)
from ..services.email_service import send_notification_email
from .auth import get_current_user
router = APIRouter(prefix="/notifications", tags=["notifications"])
@@ -29,7 +30,7 @@ def create_notification(
related_id: Optional[int] = None,
related_type: Optional[str] = None
) -> Notification:
"""Create a new notification"""
"""Create a new notification and send email"""
notification = Notification(
user_id=user_id,
notification_type=notification_type,
@@ -42,6 +43,23 @@ def create_notification(
db.add(notification)
db.commit()
db.refresh(notification)
# Send email notification (background thread, non-blocking)
try:
user = db.query(User).filter(User.id == user_id).first()
if user and user.email:
send_notification_email(
to_email=user.email,
notification_type=notification_type,
title=title,
message=message,
link=link,
user_name=user.name,
user_country=user.country,
)
except Exception as e:
print(f"[WARN] Email notification failed for user {user_id}: {e}")
return notification

View File

@@ -0,0 +1,187 @@
"""
Email Notification Service
Sends email notifications to users for important events (recommendations, shipping, payments, etc.)
Reuses existing SMTP configuration (Gmail).
"""
import smtplib
import threading
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import Optional
from ..config import get_settings
settings = get_settings()
# Notification types that should trigger email
EMAIL_ENABLED_TYPES = {
"vehicle_recommended",
"shipping_update",
"payment_confirmed",
"withdrawal_processed",
"inquiry_reply",
"dealer_approved",
"dealer_rejected",
"referral_reward",
"share_purchased",
}
def get_user_language(country: Optional[str]) -> str:
"""Detect language from user's country"""
if not country:
return "en"
c = country.lower().strip()
if c in ("korea", "south korea", "kr", "한국"):
return "ko"
if c in ("mongolia", "mn", "монгол"):
return "mn"
if c in ("russia", "ru", "россия"):
return "ru"
return "en"
def _build_email_content(
notification_type: str,
title: str,
message: str,
link: Optional[str],
user_name: Optional[str],
language: str,
) -> tuple[str, str]:
"""Build email subject and HTML body for a notification.
Returns (subject, html_body).
"""
base_url = "https://autonetsellcar.com"
full_link = f"{base_url}{link}" if link else base_url
# Greeting by language
greetings = {
"en": f"Hi {user_name}," if user_name else "Hello,",
"ko": f"{user_name}님, 안녕하세요." if user_name else "안녕하세요,",
"mn": f"Сайн байна уу, {user_name}." if user_name else "Сайн байна уу,",
"ru": f"Здравствуйте, {user_name}." if user_name else "Здравствуйте,",
}
button_labels = {
"en": "View Details",
"ko": "자세히 보기",
"mn": "Дэлгэрэнгүй үзэх",
"ru": "Подробнее",
}
footers = {
"en": "You received this email because you have an account on AutonetSellCar.com",
"ko": "AutonetSellCar.com 계정이 있어 이 이메일을 받으셨습니다",
"mn": "Та AutonetSellCar.com-д бүртгэлтэй тул энэ имэйлийг хүлээн авлаа",
"ru": "Вы получили это письмо, так как у вас есть аккаунт на AutonetSellCar.com",
}
greeting = greetings.get(language, greetings["en"])
button_label = button_labels.get(language, button_labels["en"])
footer = footers.get(language, footers["en"])
subject = f"AutonetSellCar - {title}"
html_body = f"""\
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="margin:0;padding:0;background:#f4f4f4;font-family:Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f4;padding:20px 0;">
<tr><td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);">
<!-- Header -->
<tr>
<td style="background:#1a1a2e;padding:24px 32px;">
<h1 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">AutonetSellCar</h1>
</td>
</tr>
<!-- Body -->
<tr>
<td style="padding:32px;">
<p style="margin:0 0 16px;color:#333;font-size:15px;">{greeting}</p>
<h2 style="margin:0 0 12px;color:#1a1a2e;font-size:18px;">{title}</h2>
<p style="margin:0 0 24px;color:#555;font-size:15px;line-height:1.6;">{message}</p>
<a href="{full_link}"
style="display:inline-block;background:#2563eb;color:#ffffff;text-decoration:none;padding:12px 28px;border-radius:6px;font-size:14px;font-weight:600;">
{button_label}
</a>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding:20px 32px;background:#f9fafb;border-top:1px solid #e5e7eb;">
<p style="margin:0;color:#9ca3af;font-size:12px;">{footer}</p>
</td>
</tr>
</table>
</td></tr>
</table>
</body>
</html>"""
return subject, html_body
def _send_smtp(to_email: str, subject: str, html_body: str) -> bool:
"""Send an email via SMTP. Returns True on success."""
try:
if not settings.SMTP_USER or not settings.SMTP_PASSWORD:
print(f"[DEV] Notification email to {to_email}: {subject}")
return True
msg = MIMEMultipart("alternative")
msg["From"] = f"{settings.SMTP_FROM_NAME} <{settings.SMTP_FROM_EMAIL or settings.SMTP_USER}>"
msg["To"] = to_email
msg["Subject"] = subject
msg.attach(MIMEText(html_body, "html", "utf-8"))
with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server:
server.starttls()
server.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
server.send_message(msg)
print(f"[INFO] Notification email sent to {to_email}: {subject}")
return True
except Exception as e:
print(f"[ERROR] Failed to send notification email to {to_email}: {e}")
return False
def send_notification_email(
to_email: str,
notification_type: str,
title: str,
message: str,
link: Optional[str] = None,
user_name: Optional[str] = None,
user_country: Optional[str] = None,
) -> None:
"""Send a notification email in a background thread (non-blocking).
Only sends for notification types in EMAIL_ENABLED_TYPES.
"""
if notification_type not in EMAIL_ENABLED_TYPES:
return
if not to_email:
return
language = get_user_language(user_country)
subject, html_body = _build_email_content(
notification_type=notification_type,
title=title,
message=message,
link=link,
user_name=user_name,
language=language,
)
# Send in background thread to avoid blocking the request
thread = threading.Thread(
target=_send_smtp,
args=(to_email, subject, html_body),
daemon=True,
)
thread.start()