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,
|
NotificationCreate, NotificationResponse,
|
||||||
NotificationListResponse, NotificationMarkRead
|
NotificationListResponse, NotificationMarkRead
|
||||||
)
|
)
|
||||||
|
from ..services.email_service import send_notification_email
|
||||||
from .auth import get_current_user
|
from .auth import get_current_user
|
||||||
|
|
||||||
router = APIRouter(prefix="/notifications", tags=["notifications"])
|
router = APIRouter(prefix="/notifications", tags=["notifications"])
|
||||||
@@ -29,7 +30,7 @@ def create_notification(
|
|||||||
related_id: Optional[int] = None,
|
related_id: Optional[int] = None,
|
||||||
related_type: Optional[str] = None
|
related_type: Optional[str] = None
|
||||||
) -> Notification:
|
) -> Notification:
|
||||||
"""Create a new notification"""
|
"""Create a new notification and send email"""
|
||||||
notification = Notification(
|
notification = Notification(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
notification_type=notification_type,
|
notification_type=notification_type,
|
||||||
@@ -42,6 +43,23 @@ def create_notification(
|
|||||||
db.add(notification)
|
db.add(notification)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(notification)
|
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
|
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