Files
xiaoxia-saas/packages/adapters/smtp/email_service.py
T
xiaoxia 53fb25efcf
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 48s
CI/CD Pipeline / Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m30s
CI/CD Pipeline / Validate - Style (push) Successful in 4m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m33s
CI/CD Pipeline / Validate - Security (push) Successful in 7m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Unit Tests (push) Successful in 10m11s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Failing after 26h14m3s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 26h24m21s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 26h19m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 26h23m44s
CI/CD Pipeline / PR Build API Image (push) Failing after 26h23m44s
CI/CD Pipeline / Deploy Production (push) Failing after 26h13m23s
CI/CD Pipeline / Build Production Web Image (push) Failing after 26h13m26s
CI/CD Pipeline / CI Gate (push) Failing after 26h13m25s
CI/CD Pipeline / Build Production API Image (push) Failing after 26h13m26s
CI/CD Pipeline / Canary Release to Production (push) Failing after 26h13m23s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 26h19m46s
CI/CD Pipeline / Frontend Lint (push) Failing after 26h23m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 26h23m45s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 26h19m46s
fix(#1834): 批量修复 UP 系列静态分析警告(UP007/UP006/UP017/UP035) (#1928)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-15 12:59:17 +08:00

248 lines
8.0 KiB
Python
Executable File
Raw 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.
"""
邮件服务
支持 SMTP 发送邮件(验证/重置密码/邀请等)
"""
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Optional
from packages.domain.auth.email_service import EmailConfig, EmailServicePort
class NoopEmailService:
def send_verification_email(self, **kwargs):
return False, "Email delivery is disabled"
def send_password_reset_email(self, **kwargs):
return False, "Email delivery is disabled"
class EmailService(EmailServicePort):
"""邮件服务类"""
def __init__(self, config: Optional[EmailConfig] = None):
"""
初始化邮件服务
Args:
config: 邮件配置
"""
self.config = config or EmailConfig()
def send_email(
self,
to_email: str,
subject: str,
html_body: str,
text_body: Optional[str] = None,
cc: Optional[list[str]] = None,
bcc: Optional[list[str]] = None,
) -> tuple[bool, Optional[str]]:
"""
发送邮件
Args:
to_email: 收件人邮箱
subject: 邮件主题
html_body: HTML 正文
text_body: 纯文本正文(可选,作为 HTML 的备用)
cc: 抄送列表
bcc: 密送列表
Returns:
(是否成功, 错误信息)
"""
try:
# 创建邮件
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = f"{self.config.from_name} <{self.config.from_email}>"
msg["To"] = to_email
if cc:
msg["Cc"] = ", ".join(cc)
# 添加纯文本正文
if text_body:
part1 = MIMEText(text_body, "plain", "utf-8")
msg.attach(part1)
# 添加 HTML 正文
part2 = MIMEText(html_body, "html", "utf-8")
msg.attach(part2)
# 连接 SMTP 服务器
with smtplib.SMTP(self.config.smtp_host, self.config.smtp_port) as server:
if self.config.use_tls:
server.starttls()
# 登录
if self.config.smtp_user and self.config.smtp_password:
server.login(self.config.smtp_user, self.config.smtp_password)
# 发送
recipients = [to_email]
if cc:
recipients.extend(cc)
if bcc:
recipients.extend(bcc)
server.sendmail(self.config.from_email, recipients, msg.as_string())
return True, None
except Exception as e:
return False, str(e)
def send_verification_email( # type: ignore[override]
self,
to_email: str,
username: str,
verification_url: str,
) -> tuple[bool, Optional[str]]:
"""
发送邮箱验证邮件
Args:
to_email: 收件人邮箱
username: 用户名
verification_url: 验证链接
Returns:
(是否成功, 错误信息)
"""
subject = "验证您的邮箱 - 小虾 SaaS"
html_body = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #2563eb;">欢迎加入小虾 SaaS!</h2>
<p>你好 <strong>{username}</strong>,</p>
<p>感谢您注册小虾 SaaS!请点击下面的按钮验证您的邮箱地址:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{verification_url}"
style="background-color: #2563eb; color: white; padding: 12px 30px;
text-decoration: none; border-radius: 5px; display: inline-block;">
验证邮箱
</a>
</div>
<p style="color: #666; font-size: 14px;">
如果按钮无法点击,请复制以下链接到浏览器:<br>
<a href="{verification_url}">{verification_url}</a>
</p>
<p style="color: #666; font-size: 14px;">
此链接将在 24 小时后过期。
</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
<p style="color: #999; font-size: 12px;">
如果您没有注册小虾 SaaS,请忽略此邮件。
</p>
</div>
</body>
</html>
"""
text_body = f"""
欢迎加入小虾 SaaS!
你好 {username},
感谢您注册小虾 SaaS!请访问以下链接验证您的邮箱地址:
{verification_url}
此链接将在 24 小时后过期。
如果您没有注册小虾 SaaS,请忽略此邮件。
"""
return self.send_email(to_email, subject, html_body, text_body)
def send_password_reset_email( # type: ignore[override]
self,
to_email: str,
username: str,
reset_url: str,
) -> tuple[bool, Optional[str]]:
"""
发送密码重置邮件
Args:
to_email: 收件人邮箱
username: 用户名
reset_url: 重置链接
Returns:
(是否成功, 错误信息)
"""
subject = "重置您的密码 - 小虾 SaaS"
html_body = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #dc2626;">重置密码请求</h2>
<p>你好 <strong>{username}</strong>,</p>
<p>我们收到了重置您账号密码的请求。请点击下面的按钮重置密码:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{reset_url}"
style="background-color: #dc2626; color: white; padding: 12px 30px;
text-decoration: none; border-radius: 5px; display: inline-block;">
重置密码
</a>
</div>
<p style="color: #666; font-size: 14px;">
如果按钮无法点击,请复制以下链接到浏览器:<br>
<a href="{reset_url}">{reset_url}</a>
</p>
<p style="color: #666; font-size: 14px;">
此链接将在 1 小时后过期。
</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
<p style="color: #999; font-size: 12px;">
如果您没有请求重置密码,请忽略此邮件,您的密码不会被更改。
</p>
</div>
</body>
</html>
"""
text_body = f"""
重置密码请求
你好 {username},
我们收到了重置您账号密码的请求。请访问以下链接重置密码:
{reset_url}
此链接将在 1 小时后过期。
如果您没有请求重置密码,请忽略此邮件,您的密码不会被更改。
"""
return self.send_email(to_email, subject, html_body, text_body)
_email_service = None
def get_email_service(config: Optional[EmailConfig] = None, enabled: bool = True):
global _email_service
if not enabled:
return NoopEmailService()
if _email_service is None or config is not None:
_email_service = EmailService(config)
return _email_service