修复邮件系统问题:1. 优化邮件存储逻辑确保mailbox_id正确赋值 2. 修复SMTP服务处理逻辑 3. 添加系统诊断接口 4. 新增测试脚本

This commit is contained in:
huangzhenpc
2025-02-26 13:36:40 +08:00
parent 34b1047481
commit a99d59823c
4 changed files with 729 additions and 200 deletions

View File

@@ -32,115 +32,36 @@ class MailStore:
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): 成功标志和错误信息
"""
"""保存邮件到数据库和文件系统"""
logging.info(f"开始保存邮件: 发件人={sender}, 收件人={recipients}")
try:
logging.info(f"开始保存邮件: 发件人={sender}, 收件人={recipients}")
# 从消息对象中提取主题
subject = message.get('Subject', '')
if subject is None:
subject = ''
logging.info(f"邮件主题: {subject}")
# 获取邮件内容文本和HTML
# 解析邮件内容
email_subject = None
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)}")
# 提取邮件主题
if hasattr(message, 'subject') and message.subject:
email_subject = message.subject
# 处理单部分邮件
else:
logging.info("处理单部分邮件")
content_type = message.get_content_type()
logging.debug(f"单部分邮件类型: {content_type}")
# 提取邮件内容
if hasattr(message, 'get_body'):
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')
for part in message.walk():
content_type = part.get_content_type()
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
if content_type == "text/plain":
body_text = part.get_payload(decode=True).decode(part.get_content_charset() or 'utf-8', errors='replace')
elif content_type == "text/html":
body_html = part.get_payload(decode=True).decode(part.get_content_charset() or 'utf-8', errors='replace')
# 处理附件
if part.get_filename():
file_name = part.get_filename()
content = part.get_payload(decode=True)
attachments.append((file_name, content))
except Exception as e:
logging.error(f"获取邮件内容时出错: {str(e)}")
@@ -165,87 +86,126 @@ class MailStore:
mailbox_id = None
recipients_list = recipients if isinstance(recipients, list) else [recipients]
# 确保收件人列表不为空
if not recipients_list:
logging.error("收件人列表为空,无法确定邮箱")
return False, "收件人列表为空"
# 尝试找到或创建收件人邮箱
for recipient in recipients_list:
# 跳过空收件人
if not recipient or '@' not in recipient:
continue
# 提取域名和用户名
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
username, domain = recipient.lower().split('@', 1)
logging.info(f"处理收件人: 用户名={username}, 域名={domain}")
# 1. 先查找域名
domain_obj = session.query(Domain).filter(Domain.name.ilike(domain)).first()
# 如果域名不存在,创建它
if not domain_obj:
logging.info(f"创建新域名: {domain}")
domain_obj = Domain(
name=domain.lower(),
description=f"系统自动创建的域名 ({domain})",
active=True
)
session.add(domain_obj)
session.flush()
# 2. 查找或创建邮箱
mailbox = session.query(Mailbox).filter(
Mailbox.domain_id == domain_obj.id,
Mailbox.address.ilike(username)
).first()
# 如果邮箱不存在,创建它
if not mailbox:
logging.info(f"创建新邮箱: {username}@{domain}")
mailbox = Mailbox(
domain_id=domain_obj.id,
address=username.lower(),
description=f"系统自动创建的邮箱 ({username}@{domain})"
)
session.add(mailbox)
session.flush()
# 设置邮箱ID并结束循环
mailbox_id = mailbox.id
logging.info(f"使用邮箱ID: {mailbox_id} ({username}@{domain})")
break
if not mailbox_id:
logging.error(f"收件人 {recipients} 没有对应的邮箱记录")
return False, "收件邮箱不存在"
# 最终检查是否获取到了邮箱ID
if mailbox_id is None:
error_msg = f"无法确定有效的收件邮箱,无法保存邮件。收件人: {recipients}"
logging.error(error_msg)
return False, error_msg
# 创建邮件记录
new_email = Email(
mailbox_id=mailbox_id, # 设置邮箱ID
subject=subject,
# 创建邮件记录
email_obj = Email(
mailbox_id=mailbox_id, # 确保始终有邮箱ID
sender=sender,
recipients=','.join(recipients_list) if len(recipients_list) > 1 else recipients_list[0],
recipients=str(recipients),
subject=email_subject,
body_text=body_text,
body_html=body_html,
received_at=datetime.now()
)
# 提取验证码和验证链接(如果有)
new_email.extract_verification_data()
# 提取验证码和验证链接
email_obj.extract_verification_data()
session.add(email_obj)
session.flush() # 获取新创建邮件的ID
# 保存附件
for file_name, content in attachments:
attachment = Attachment(
email_id=email_obj.id,
filename=file_name,
content_type="application/octet-stream",
size=len(content)
)
session.add(attachment)
# 保存附件内容到文件
attachment_path = os.path.join(self.storage_path, 'attachments', f"attachment_{attachment.id}")
os.makedirs(os.path.dirname(attachment_path), exist_ok=True)
with open(attachment_path, 'wb') as f:
f.write(content)
# 保存原始邮件数据
raw_path = os.path.join(self.storage_path, 'emails', f"email_{email_obj.id}.eml")
os.makedirs(os.path.dirname(raw_path), exist_ok=True)
# 写入原始邮件
with open(raw_path, 'w', encoding='utf-8', errors='replace') as f:
if isinstance(message, str):
f.write(message)
else:
# 如果是邮件对象,尝试获取原始文本
try:
f.write(message.as_string())
except Exception:
# 如果失败,使用提供的原始数据
if raw_data:
f.write(raw_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}"
logging.info(f"邮件保存成功: ID={email_obj.id}")
return True, f"邮件保存: ID={email_obj.id}"
except Exception as e:
logging.error(f"保存邮件到数据库失败: {str(e)}")
return False, f"保存邮件失败: {str(e)}"
session.rollback()
logging.error(f"数据库操作失败: {str(e)}")
raise
finally:
session.close()
except Exception as e:
logging.error(f"保存邮件时出现未处理异常: {str(e)}")
import traceback
traceback.print_exc()
return False, f"保存邮件过程中出错: {str(e)}"
logging.error(f"邮件保存失败: {str(e)}")
return False, f"保存邮件失败: {str(e)}"
def get_emails_for_mailbox(self, mailbox_id, limit=50, offset=0, unread_only=False):
"""获取指定邮箱的邮件列表"""

View File

@@ -37,45 +37,38 @@ class EmailHandler(Message):
return
async def handle_DATA(self, server, session, envelope):
"""处理接收到的邮件数据"""
"""处理邮件数据"""
try:
logging.info(f"收到邮件: 发件人={envelope.mail_from}, 收件人={envelope.rcpt_tos}")
# 获取邮件数据
message_data = envelope.content.decode('utf-8', errors='replace')
# 保存原始邮件数据
data = envelope.content.decode('utf-8', errors='replace')
# 记录接收到的邮件
sender = envelope.mail_from
recipients = envelope.rcpt_tos
# 解析邮件数据
message = email_parser.Parser().parsestr(data)
subject = message.get('Subject', '')
logging.info(f"邮件主题: {subject}")
logging.info(f"SMTP服务收到邮件: 发件人={sender}, 收件人={recipients}")
# 记录邮件结构和内容
logging.debug(f"邮件结构: is_multipart={message.is_multipart()}")
if message.is_multipart():
logging.debug(f"多部分邮件: 部分数量={len(list(message.walk()))}")
for i, part in enumerate(message.walk()):
content_type = part.get_content_type()
logging.debug(f"部分 {i+1}: 内容类型={content_type}")
# 使用email.parser解析邮件
parser = email.parser.Parser(policy=default)
message = parser.parsestr(message_data)
# 使用邮件存储服务保存邮件
success, error_msg = await self.mail_store.save_email(
message,
envelope.mail_from,
envelope.rcpt_tos,
raw_data=data
)
# 保存邮件
success, result_message = await self.mail_store.save_email(message, sender, recipients, message_data)
if success:
logging.info(f"邮件保存成功: 来自 {envelope.mail_from} 发送给 {envelope.rcpt_tos}")
return '250 消息接收完成'
logging.info(f"邮件已成功保存: {result_message}")
return '250 Message accepted for delivery'
else:
logging.error(f"邮件保存失败: {error_msg}")
return '451 处理邮件时出现错误,请稍后重试'
logging.error(f"邮件保存失败: {result_message}")
# 注意:即使保存失败,我们仍然返回成功,避免发件方重试
# 这是因为问题通常在我们的系统中,重试不会解决问题
return '250 Message accepted for delivery (warning: internal processing error)'
except Exception as e:
logging.error(f"处理邮件时出错: {str(e)}")
traceback.print_exc()
return '451 处理邮件时出现错误,请稍后重试'
# 返回临时错误,让发件方可以重试
return '451 Requested action aborted: local error in processing'
# 为Windows环境自定义SMTP控制器