hku-class/backend/app/services/email_service.py
2026-04-27 22:36:48 +08:00

223 lines
7.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import aiosmtplib
from app.config import settings
logger = logging.getLogger(__name__)
def build_email_shell(
*,
eyebrow: str,
title: str,
body_html: str,
action_label: str | None = None,
action_url: str | None = None,
summary_html: str | None = None,
footer_note: str = "此邮件由系统自动发送,请勿直接回复。",
) -> str:
action_html = ""
if action_label and action_url:
action_html = f"""
<div style="margin-top:28px;">
<a
href="{action_url}"
style="
display:inline-block;
padding:12px 22px;
border-radius:999px;
background:linear-gradient(135deg,#6c1a25 0%,#915435 62%,#d9b68d 130%);
color:#fffaf3;
text-decoration:none;
font-size:14px;
font-weight:600;
letter-spacing:0.01em;
box-shadow:0 14px 32px -18px rgba(83,25,24,0.55);
"
>
{action_label}
</a>
</div>
"""
summary_block = ""
if summary_html:
summary_block = f"""
<div
style="
margin:18px 0 0;
padding:14px 16px;
border:1px solid #eadcc8;
border-radius:18px;
background:#fff4e3;
font-size:13px;
line-height:1.8;
color:#84553c;
"
>
{summary_html}
</div>
"""
return f"""
<!doctype html>
<html lang="zh-CN">
<body style="margin:0;padding:0;background:#f7efe2;">
<div style="margin:0;padding:32px 16px;background:linear-gradient(180deg,#f7efe2 0%,#f4ebdf 38%,#f9f5ee 100%);">
<div style="max-width:640px;margin:0 auto;">
<div
style="
overflow:hidden;
border:1px solid #e7d3ba;
border-radius:28px;
background:#fffaf3;
box-shadow:0 28px 80px -44px rgba(83,25,24,0.34);
"
>
<div
style="
padding:32px 36px 28px;
background:
radial-gradient(circle at top, rgba(115,25,37,0.18), transparent 58%),
linear-gradient(135deg, rgba(108,26,37,0.98), rgba(145,84,53,0.92) 58%, rgba(233,206,160,0.88) 140%);
color:#fff7ee;
"
>
<div
style="
display:inline-block;
padding:7px 12px;
border:1px solid rgba(255,245,231,0.28);
border-radius:999px;
background:rgba(255,250,242,0.08);
font-size:11px;
letter-spacing:0.24em;
text-transform:uppercase;
"
>
{eyebrow}
</div>
<h1 style="margin:18px 0 0;font-size:30px;line-height:1.22;font-weight:700;">
香港大学中国商业学院
</h1>
<p style="margin:8px 0 0;font-size:14px;line-height:1.7;color:rgba(255,247,238,0.86);">
HKU ICB · 班级信息管理平台
</p>
</div>
<div style="padding:32px 36px 14px;">
<h2 style="margin:0 0 14px;font-size:24px;line-height:1.35;color:#4e1d1a;">
{title}
</h2>
<div style="font-size:15px;line-height:1.9;color:#775a4a;">
{body_html}
</div>
{summary_block}
{action_html}
</div>
<div style="padding:18px 36px 30px;">
<div
style="
border-top:1px solid #eadcc8;
padding-top:16px;
font-size:12px;
line-height:1.8;
color:#9a7b68;
"
>
{footer_note}
</div>
</div>
</div>
</div>
</div>
</body>
</html>
"""
async def send_email(to: str, subject: str, html_body: str) -> bool:
"""Send HTML email via SMTP. Returns True on success."""
if not settings.smtp_host:
logger.info(f"SMTP not configured, skipping email to {to}: {subject}")
return False
msg = MIMEMultipart("alternative")
msg["From"] = f"{settings.smtp_from_name} <{settings.smtp_from_email}>"
msg["To"] = to
msg["Subject"] = subject
msg.attach(MIMEText(html_body, "html"))
try:
await aiosmtplib.send(
msg,
hostname=settings.smtp_host,
port=settings.smtp_port,
username=settings.smtp_user,
password=settings.smtp_password,
use_tls=True,
)
return True
except Exception as e:
logger.error(f"Failed to send email to {to}: {e}")
return False
async def send_account_activated_email(member_email: str) -> bool:
login_url = f"{settings.frontend_url.rstrip('/')}/login"
html = build_email_shell(
eyebrow="Account Ready",
title="账号已激活,可以开始使用",
body_html="""
<p style="margin:0 0 12px;">你的 HKU ICB 班级账号已经成功激活。</p>
<p style="margin:0;">现在可以使用注册邮箱登录平台,进入班级空间查看公告、排期、资源与成员信息。</p>
""",
action_label="立即登录",
action_url=login_url,
)
return await send_email(member_email, "HKU ICB账号激活成功", html)
async def send_email_verification_code_email(member_email: str, code: str) -> bool:
html = build_email_shell(
eyebrow="Email Verification",
title="请验证你的邮箱地址",
body_html="""
<p style="margin:0 0 12px;">你正在激活 HKU ICB 班级账号,请使用下方验证码完成邮箱验证。</p>
<p style="margin:0;">验证码 10 分钟内有效;如果不是你本人操作,可以忽略此邮件。</p>
""",
summary_html=f"""
<div style="font-size:12px;letter-spacing:0.18em;text-transform:uppercase;color:#9a7b68;">邮箱验证码</div>
<div style="margin-top:8px;font-size:32px;line-height:1.2;font-weight:700;letter-spacing:0.32em;color:#4e1d1a;">{code}</div>
""",
)
return await send_email(member_email, "HKU ICB邮箱验证码", html)
async def send_class_notification_email(
emails: list[str],
subject: str,
title: str,
body: str,
action_url: str | None = None,
eyebrow: str = "Class Update",
action_label: str | None = "查看详情",
summary_html: str | None = None,
):
"""Send a styled notification email to class members."""
normalized_body = body.replace("\n", "<br />")
html = build_email_shell(
eyebrow=eyebrow,
title=title,
body_html=f"<div>{normalized_body}</div>",
action_label=action_label if action_url else None,
action_url=action_url,
summary_html=summary_html,
)
for email in emails:
await send_email(email, subject, html)