初始化提交,包含完整的邮件系统代码
This commit is contained in:
44
app/services/__init__.py
Normal file
44
app/services/__init__.py
Normal 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'
|
||||
]
|
||||
123
app/services/email_processor.py
Normal file
123
app/services/email_processor.py
Normal 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
263
app/services/mail_store.py
Normal 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
136
app/services/smtp_server.py
Normal 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
|
||||
Reference in New Issue
Block a user