初始化提交,包含完整的邮件系统代码

This commit is contained in:
huangzhenpc
2025-02-25 19:50:00 +08:00
commit aeffc4f8b8
52 changed files with 6673 additions and 0 deletions

44
app/services/__init__.py Normal file
View File

@@ -0,0 +1,44 @@
# 服务层初始化文件
# 这里将导入所有服务模块以便于统一调用
from .smtp_server import SMTPServer
from .email_processor import EmailProcessor
from .mail_store import MailStore
# 全局服务实例
_smtp_server = None
_email_processor = None
_mail_store = None
def register_smtp_server(instance):
"""注册SMTP服务器实例"""
global _smtp_server
_smtp_server = instance
def register_email_processor(instance):
"""注册邮件处理器实例"""
global _email_processor
_email_processor = instance
def register_mail_store(instance):
"""注册邮件存储实例"""
global _mail_store
_mail_store = instance
def get_smtp_server():
"""获取SMTP服务器实例"""
return _smtp_server
def get_email_processor():
"""获取邮件处理器实例"""
return _email_processor
def get_mail_store():
"""获取邮件存储实例"""
return _mail_store
__all__ = [
'SMTPServer', 'EmailProcessor', 'MailStore',
'register_smtp_server', 'register_email_processor', 'register_mail_store',
'get_smtp_server', 'get_email_processor', 'get_mail_store'
]

View File

@@ -0,0 +1,123 @@
import logging
import re
import threading
import time
from queue import Queue
logger = logging.getLogger(__name__)
class EmailProcessor:
"""邮件处理器,负责处理邮件并提取验证信息"""
def __init__(self, mail_store):
"""
初始化邮件处理器
参数:
mail_store: 邮件存储服务实例
"""
self.mail_store = mail_store
self.processing_queue = Queue()
self.is_running = False
self.worker_thread = None
def start(self):
"""启动邮件处理器"""
if self.is_running:
logger.warning("邮件处理器已在运行")
return False
self.is_running = True
self.worker_thread = threading.Thread(
target=self._processing_worker,
daemon=True
)
self.worker_thread.start()
logger.info("邮件处理器已启动")
return True
def stop(self):
"""停止邮件处理器"""
if not self.is_running:
logger.warning("邮件处理器未在运行")
return False
self.is_running = False
if self.worker_thread:
self.worker_thread.join(timeout=5.0)
self.worker_thread = None
logger.info("邮件处理器已停止")
return True
def queue_email_for_processing(self, email_id):
"""将邮件添加到处理队列"""
self.processing_queue.put(email_id)
return True
def _processing_worker(self):
"""处理队列中的邮件的工作线程"""
while self.is_running:
try:
# 获取队列中的邮件最多等待1秒
try:
email_id = self.processing_queue.get(timeout=1.0)
except:
continue
# 处理邮件
self._process_email(email_id)
# 标记任务完成
self.processing_queue.task_done()
except Exception as e:
logger.error(f"处理邮件时出错: {str(e)}")
def _process_email(self, email_id):
"""处理单个邮件,提取验证码和链接"""
# 从邮件存储获取邮件
email_data = self.mail_store.get_email_by_id(email_id, mark_as_read=False)
if not email_data:
logger.warning(f"找不到ID为 {email_id} 的邮件")
return False
# 提取验证码和链接已经在Email模型的extract_verification_data方法中实现
# 这里可以添加更复杂的提取逻辑或后处理
logger.info(f"邮件 {email_id} 处理完成")
return True
@staticmethod
def extract_verification_code(content):
"""从内容中提取验证码"""
code_patterns = [
r'\b[A-Z0-9]{4,8}\b', # 基本验证码格式
r'验证码[:]\s*([A-Z0-9]{4,8})',
r'验证码是[:]\s*([A-Z0-9]{4,8})',
r'code[:]\s*([A-Z0-9]{4,8})',
r'码[:]\s*(\d{4,8})' # 纯数字验证码
]
for pattern in code_patterns:
matches = re.findall(pattern, content, re.IGNORECASE)
if matches:
return matches[0]
return None
@staticmethod
def extract_verification_link(content):
"""从内容中提取验证链接"""
link_patterns = [
r'(https?://\S+(?:verify|confirm|activate)\S+)',
r'(https?://\S+(?:token|auth|account)\S+)',
r'href\s*=\s*["\']([^"\']+(?:verify|confirm|activate)[^"\']*)["\']'
]
for pattern in link_patterns:
matches = re.findall(pattern, content, re.IGNORECASE)
if matches:
return matches[0]
return None

263
app/services/mail_store.py Normal file
View File

@@ -0,0 +1,263 @@
import logging
import os
import email
from email.policy import default
from sqlalchemy.orm import Session
from datetime import datetime
import re
from ..models.domain import Domain
from ..models.mailbox import Mailbox
from ..models.email import Email
from ..models.attachment import Attachment
logger = logging.getLogger(__name__)
class MailStore:
"""邮件存储服务,负责保存和检索邮件"""
def __init__(self, db_session_factory, storage_path=None):
"""
初始化邮件存储服务
参数:
db_session_factory: 数据库会话工厂函数
storage_path: 附件存储路径
"""
self.db_session_factory = db_session_factory
self.storage_path = storage_path or os.path.join(os.getcwd(), 'email_data')
# 确保存储目录存在
if not os.path.exists(self.storage_path):
os.makedirs(self.storage_path)
async def save_email(self, sender, recipient, message, raw_data):
"""
保存一封电子邮件
参数:
sender: 发件人地址
recipient: 收件人地址
message: 解析后的邮件对象
raw_data: 原始邮件数据
返回:
成功返回邮件ID失败返回None
"""
# 从收件人地址中提取用户名和域名
try:
address, domain_name = recipient.split('@', 1)
except ValueError:
logger.warning(f"无效的收件人地址格式: {recipient}")
return None
# 获取数据库会话
db = self.db_session_factory()
try:
# 检查域名是否存在且活跃
domain = db.query(Domain).filter_by(name=domain_name, active=True).first()
if not domain:
logger.warning(f"不支持的域名: {domain_name}")
return None
# 查找或创建邮箱
mailbox = db.query(Mailbox).filter_by(address=address, domain_id=domain.id).first()
if not mailbox:
# 自动创建新邮箱
mailbox = Mailbox(
address=address,
domain_id=domain.id,
active=True
)
db.add(mailbox)
db.flush() # 获取ID但不提交
logger.info(f"已为 {recipient} 自动创建邮箱")
# 提取邮件内容
subject = message.get('subject', '')
# 获取文本和HTML内容
body_text = None
body_html = None
attachments_data = []
if message.is_multipart():
for part in message.walk():
content_type = part.get_content_type()
content_disposition = part.get_content_disposition()
# 处理文本内容
if content_disposition is None or content_disposition == 'inline':
if content_type == 'text/plain' and not body_text:
body_text = part.get_content()
elif content_type == 'text/html' and not body_html:
body_html = part.get_content()
# 处理附件
elif content_disposition == 'attachment':
filename = part.get_filename()
if filename:
content = part.get_payload(decode=True)
if content:
attachments_data.append({
'filename': filename,
'content_type': content_type,
'data': content,
'size': len(content)
})
else:
# 非多部分邮件
content_type = message.get_content_type()
if content_type == 'text/plain':
body_text = message.get_content()
elif content_type == 'text/html':
body_html = message.get_content()
# 创建邮件记录
email_obj = Email(
mailbox_id=mailbox.id,
sender=sender,
recipients=recipient,
subject=subject,
body_text=body_text,
body_html=body_html,
headers={k: v for k, v in message.items()}
)
# 保存邮件
db.add(email_obj)
db.flush() # 获取ID但不提交
# 提取验证信息
email_obj.extract_verification_data()
# 保存附件
for attachment_data in attachments_data:
attachment = Attachment(
email_id=email_obj.id,
filename=attachment_data['filename'],
content_type=attachment_data['content_type'],
size=attachment_data['size']
)
db.add(attachment)
db.flush()
# 决定存储位置
if attachment_data['size'] > 1024 * 1024: # 大于1MB的存储到文件系统
attachments_dir = os.path.join(self.storage_path, 'attachments')
attachment.save_to_filesystem(attachment_data['data'], attachments_dir)
else:
# 小附件直接存储在数据库
attachment.content = attachment_data['data']
# 提交所有更改
db.commit()
logger.info(f"邮件已成功保存: {sender} -> {recipient}, ID: {email_obj.id}")
return email_obj.id
except Exception as e:
db.rollback()
logger.error(f"保存邮件时出错: {str(e)}")
return None
finally:
db.close()
def get_emails_for_mailbox(self, mailbox_id, limit=50, offset=0, unread_only=False):
"""获取指定邮箱的邮件列表"""
db = self.db_session_factory()
try:
query = db.query(Email).filter(Email.mailbox_id == mailbox_id)
if unread_only:
query = query.filter(Email.read == False)
total = query.count()
emails = query.order_by(Email.received_at.desc()) \
.limit(limit) \
.offset(offset) \
.all()
return {
'total': total,
'items': [email.to_dict() for email in emails]
}
except Exception as e:
logger.error(f"获取邮件列表时出错: {str(e)}")
return {'total': 0, 'items': []}
finally:
db.close()
def get_email_by_id(self, email_id, mark_as_read=True):
"""获取指定ID的邮件详情"""
db = self.db_session_factory()
try:
email = db.query(Email).filter(Email.id == email_id).first()
if not email:
return None
if mark_as_read and not email.read:
email.read = True
email.last_read = datetime.utcnow()
db.commit()
# 获取附件信息
attachments = [attachment.to_dict() for attachment in email.attachments]
# 构建完整响应
result = email.to_dict()
result['body_text'] = email.body_text
result['body_html'] = email.body_html
result['attachments'] = attachments
return result
except Exception as e:
db.rollback()
logger.error(f"获取邮件详情时出错: {str(e)}")
return None
finally:
db.close()
def delete_email(self, email_id):
"""删除指定ID的邮件"""
db = self.db_session_factory()
try:
email = db.query(Email).filter(Email.id == email_id).first()
if not email:
return False
db.delete(email)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"删除邮件时出错: {str(e)}")
return False
finally:
db.close()
def get_attachment_content(self, attachment_id):
"""获取附件内容"""
db = self.db_session_factory()
try:
attachment = db.query(Attachment).filter(Attachment.id == attachment_id).first()
if not attachment:
return None
content = attachment.get_content()
return {
'content': content,
'filename': attachment.filename,
'content_type': attachment.content_type
}
except Exception as e:
logger.error(f"获取附件内容时出错: {str(e)}")
return None
finally:
db.close()

136
app/services/smtp_server.py Normal file
View File

@@ -0,0 +1,136 @@
import asyncio
import logging
import email
import platform
from email.policy import default
from aiosmtpd.controller import Controller
from aiosmtpd.smtp import SMTP as SMTPProtocol
from aiosmtpd.handlers import Message
import os
import sys
import threading
from ..models.domain import Domain
from ..models.mailbox import Mailbox
logger = logging.getLogger(__name__)
# 检测是否Windows环境
IS_WINDOWS = platform.system().lower() == 'windows'
class EmailHandler(Message):
"""处理接收的电子邮件"""
def __init__(self, mail_store):
super().__init__()
self.mail_store = mail_store
def handle_message(self, message):
"""处理邮件消息这是Message类的抽象方法必须实现"""
# 这个方法在异步DATA处理完成后被调用但我们的邮件处理逻辑已经在handle_DATA中实现
# 所以这里只是一个空实现
return
async def handle_DATA(self, server, session, envelope):
"""处理接收到的邮件数据"""
try:
# 获取收件人和发件人
peer = session.peer
mail_from = envelope.mail_from
rcpt_tos = envelope.rcpt_tos
# 获取原始邮件内容
data = envelope.content
mail = email.message_from_bytes(data, policy=default)
# 保存邮件到存储服务
for rcpt in rcpt_tos:
result = await self.mail_store.save_email(mail_from, rcpt, mail, data)
# 记录日志
if result:
logger.info(f"邮件已保存: {mail_from} -> {rcpt}, 主题: {mail.get('Subject')}")
else:
logger.warning(f"邮件未保存: {mail_from} -> {rcpt}, 可能是无效地址")
return '250 Message accepted for delivery'
except Exception as e:
logger.error(f"处理邮件时出错: {str(e)}")
return '451 Requested action aborted: error in processing'
# 为Windows环境自定义SMTP控制器
if IS_WINDOWS:
class WindowsSafeController(Controller):
"""Windows环境安全的Controller跳过连接测试"""
def _trigger_server(self):
"""Windows环境下跳过SMTP服务器自检连接测试"""
# 在Windows环境下我们跳过自检连接测试
logger.info("Windows环境: 跳过SMTP服务器连接自检")
return
class SMTPServer:
"""SMTP服务器实现"""
def __init__(self, host='0.0.0.0', port=25, mail_store=None):
self.host = host
self.port = port
self.mail_store = mail_store
self.controller = None
self.server_thread = None
def start(self):
"""启动SMTP服务器"""
if self.controller:
logger.warning("SMTP服务器已经在运行")
return
try:
handler = EmailHandler(self.mail_store)
# 根据环境选择适当的Controller
if IS_WINDOWS:
# Windows环境使用自定义Controller
logger.info(f"Windows环境: 使用自定义Controller启动SMTP服务器 {self.host}:{self.port}")
self.controller = WindowsSafeController(
handler,
hostname=self.host,
port=self.port
)
else:
# 非Windows环境使用标准Controller
self.controller = Controller(
handler,
hostname=self.host,
port=self.port
)
# 在单独的线程中启动服务器
self.server_thread = threading.Thread(
target=self.controller.start,
daemon=True
)
self.server_thread.start()
logger.info(f"SMTP服务器已启动在 {self.host}:{self.port}")
return True
except Exception as e:
logger.error(f"启动SMTP服务器失败: {str(e)}")
return False
def stop(self):
"""停止SMTP服务器"""
if not self.controller:
logger.warning("SMTP服务器没有运行")
return
try:
self.controller.stop()
self.controller = None
self.server_thread = None
logger.info("SMTP服务器已停止")
return True
except Exception as e:
logger.error(f"停止SMTP服务器失败: {str(e)}")
return False