""" 邮件服务 支持 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"""

欢迎加入小虾 SaaS!

你好 {username}

感谢您注册小虾 SaaS!请点击下面的按钮验证您的邮箱地址:

验证邮箱

如果按钮无法点击,请复制以下链接到浏览器:
{verification_url}

此链接将在 24 小时后过期。


如果您没有注册小虾 SaaS,请忽略此邮件。

""" 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"""

重置密码请求

你好 {username}

我们收到了重置您账号密码的请求。请点击下面的按钮重置密码:

重置密码

如果按钮无法点击,请复制以下链接到浏览器:
{reset_url}

此链接将在 1 小时后过期。


如果您没有请求重置密码,请忽略此邮件,您的密码不会被更改。

""" 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