Files
emailsystem/app/services/mail_store.py

346 lines
15 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, message, sender, recipients, raw_data=None):
"""
保存邮件到数据库
Args:
message: 已解析的邮件对象
sender: 发件人邮箱
recipients: 收件人邮箱列表
raw_data: 原始邮件数据
Returns:
(bool, str): 成功标志和错误信息
"""
try:
logging.info(f"开始保存邮件: 发件人={sender}, 收件人={recipients}")
# 从消息对象中提取主题
subject = message.get('Subject', '')
if subject is None:
subject = ''
logging.info(f"邮件主题: {subject}")
# 获取邮件内容文本和HTML
body_text = ""
body_html = ""
attachments = []
# 处理多部分邮件
if message.is_multipart():
logging.info("处理多部分邮件")
for part in message.walk():
content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition") or "")
logging.debug(f"处理邮件部分: 类型={content_type}, 处置={content_disposition}")
# 处理附件
if "attachment" in content_disposition:
try:
filename = part.get_filename()
if filename:
payload = part.get_payload(decode=True)
if payload and len(payload) > 0:
logging.info(f"发现附件: {filename}, 大小={len(payload)}字节")
# 将附件信息添加到列表,稍后处理
attachments.append({
'filename': filename,
'content_type': content_type,
'data': payload
})
except Exception as e:
logging.error(f"处理附件时出错: {str(e)}")
continue
# 处理内容部分
elif content_type == "text/plain" and not body_text:
try:
payload = part.get_payload(decode=True)
if payload:
charset = part.get_content_charset() or 'utf-8'
try:
body_text = payload.decode(charset, errors='replace')
logging.info(f"提取到纯文本内容: {len(body_text)}字节")
except Exception as e:
logging.error(f"解码纯文本内容失败: {e}")
body_text = payload.decode('utf-8', errors='replace')
except Exception as e:
logging.error(f"获取纯文本部分时出错: {str(e)}")
elif content_type == "text/html" and not body_html:
try:
payload = part.get_payload(decode=True)
if payload:
charset = part.get_content_charset() or 'utf-8'
try:
body_html = payload.decode(charset, errors='replace')
logging.info(f"提取到HTML内容: {len(body_html)}字节")
except Exception as e:
logging.error(f"解码HTML内容失败: {e}")
body_html = payload.decode('utf-8', errors='replace')
except Exception as e:
logging.error(f"获取HTML部分时出错: {str(e)}")
# 处理单部分邮件
else:
logging.info("处理单部分邮件")
content_type = message.get_content_type()
logging.debug(f"单部分邮件类型: {content_type}")
try:
payload = message.get_payload(decode=True)
if payload:
charset = message.get_content_charset() or 'utf-8'
logging.debug(f"邮件编码: {charset}")
try:
decoded_content = payload.decode(charset, errors='replace')
except Exception as e:
logging.error(f"解码内容失败: {e}")
decoded_content = payload.decode('utf-8', errors='replace')
if content_type == 'text/plain':
body_text = decoded_content
logging.info(f"提取到纯文本内容: {len(body_text)}字节")
elif content_type == 'text/html':
body_html = decoded_content
logging.info(f"提取到HTML内容: {len(body_html)}字节")
else:
logging.warning(f"未知内容类型: {content_type}")
# 假设为纯文本
body_text = decoded_content
except Exception as e:
logging.error(f"获取邮件内容时出错: {str(e)}")
# 如果仍然没有内容,尝试从原始数据中提取
if not body_text and not body_html and raw_data:
logging.info("从原始数据中提取内容")
try:
# 简单提取,可能不适用于所有情况
if '<html>' in raw_data.lower():
body_html = raw_data
else:
body_text = raw_data
except Exception as e:
logging.error(f"从原始数据提取内容失败: {str(e)}")
logging.info(f"提取完成: 纯文本={len(body_text)}字节, HTML={len(body_html)}字节, 附件数={len(attachments)}")
# 保存到数据库
session = self.db_session_factory()
try:
# 查找收件人对应的邮箱
mailbox_id = None
recipients_list = recipients if isinstance(recipients, list) else [recipients]
for recipient in recipients_list:
# 提取域名和用户名
if '@' in recipient:
username, domain = recipient.split('@', 1)
logging.info(f"查找邮箱: 用户名={username}, 域名={domain}")
# 查询域名
domain_obj = session.query(Domain).filter(Domain.name == domain).first()
if domain_obj:
# 查询邮箱
mailbox = session.query(Mailbox).filter(
Mailbox.domain_id == domain_obj.id,
Mailbox.address == username
).first()
if mailbox:
mailbox_id = mailbox.id
logging.info(f"找到邮箱ID: {mailbox_id}")
break
if not mailbox_id:
logging.error(f"收件人 {recipients} 没有对应的邮箱记录")
return False, "收件人邮箱不存在"
# 创建新邮件记录
new_email = Email(
mailbox_id=mailbox_id, # 设置邮箱ID
subject=subject,
sender=sender,
recipients=','.join(recipients_list) if len(recipients_list) > 1 else recipients_list[0],
body_text=body_text,
body_html=body_html,
received_at=datetime.now()
)
# 提取验证码和验证链接(如果有)
new_email.extract_verification_data()
# 保存邮件
session.add(new_email)
session.commit()
email_id = new_email.id
logging.info(f"邮件保存到数据库, ID={email_id}")
# 处理附件
if attachments:
for attachment_data in attachments:
attachment = Attachment(
email_id=email_id,
filename=attachment_data['filename'],
content_type=attachment_data['content_type'],
size=len(attachment_data['data']),
data=attachment_data['data']
)
session.add(attachment)
session.commit()
logging.info(f"保存了{len(attachments)}个附件")
# 保存原始邮件到文件系统
try:
if raw_data and email_id:
email_dir = os.path.join(self.storage_path, 'emails')
os.makedirs(email_dir, exist_ok=True)
email_path = os.path.join(email_dir, f'email_{email_id}.eml')
with open(email_path, 'w', encoding='utf-8') as f:
f.write(raw_data)
logging.info(f"原始邮件保存到: {email_path}")
except Exception as e:
logging.error(f"保存原始邮件到文件系统失败: {str(e)}")
return True, f"邮件保存成功ID: {email_id}"
except Exception as e:
logging.error(f"保存邮件到数据库失败: {str(e)}")
return False, f"保存邮件失败: {str(e)}"
finally:
session.close()
except Exception as e:
logging.error(f"保存邮件时出现未处理异常: {str(e)}")
import traceback
traceback.print_exc()
return False, f"保存邮件过程中出错: {str(e)}"
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.now()
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()