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
248 lines
7.9 KiB
Python
Executable File
248 lines
7.9 KiB
Python
Executable File
"""
|
||
邮件服务
|
||
支持 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
|