Files
xiaoxia-saas/packages/adapters/smtp/email_service.py
xiaoxia d92909321c
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 2m12s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m28s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m24s
CI/CD Pipeline / Unit Tests (push) Successful in 6m25s
CI/CD Pipeline / Integration Tests (push) Successful in 5m4s
CI/CD Pipeline / Frontend Lint (push) Successful in 45s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m23s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m44s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m17s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m36s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
refactor(#777): 修复Domain层依赖Adapters的DDD违规(4处) (#789)
2026-07-23 23:54:56 +08:00

248 lines
7.9 KiB
Python
Executable File
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.
"""
邮件服务
支持 SMTP 发送邮件(验证/重置密码/邀请等)
"""
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import List, 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(
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(
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