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
- 删除 email_service.py 中 send_workspace_invitation_email 方法(NoopEmailService + EmailService) - 更新 auth.py 中间件注释,移除 workspace 引用 - 更新 packages/domain/permissions.py 注释 - 删除空的 routes/permissions.py 文件 项目创建链路已验证完全独立于 workspace,无功能性影响。
260 lines
8.1 KiB
Python
260 lines
8.1 KiB
Python
"""
|
||
邮件服务
|
||
支持 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
|