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:
@@ -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
|
||||
|
||||
|
||||
|
||||
187
backend/app/services/email_service.py
Normal file
187
backend/app/services/email_service.py
Normal 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()
|
||||
Reference in New Issue
Block a user