Files
xiaoxia-saas/packages/adapters/smtp/email_service.py
T
灵应 ee5b3ece7b
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Staging E2E Tests (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Failing after 140h9m10s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 140h9m10s
chore: 清理后端 workspace 残留代码
- 删除 email_service.py 中 send_workspace_invitation_email 方法(NoopEmailService + EmailService)
- 更新 auth.py 中间件注释,移除 workspace 引用
- 更新 packages/domain/permissions.py 注释
- 删除空的 routes/permissions.py 文件

项目创建链路已验证完全独立于 workspace,无功能性影响。
2026-07-03 16:45:19 +08:00

260 lines
8.1 KiB
Python
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 dataclasses import dataclass
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import List, Optional
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"
@dataclass
class EmailConfig:
"""邮件配置"""
smtp_host: str = "smtp.gmail.com"
smtp_port: int = 587
smtp_user: str = ""
smtp_password: str = ""
from_email: str = ""
from_name: str = "小虾 SaaS"
use_tls: bool = True
class EmailService:
"""邮件服务类"""
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