Files
emailsystem/app/services/mail_store.py
2025-02-25 19:50:00 +08:00

263 lines
9.3 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.

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()