增强:添加批量注册功能和支持文件

This commit is contained in:
huangzhenpc
2025-02-26 10:16:12 +08:00
parent 71c9653d54
commit a9e29c9cf5
11 changed files with 1392 additions and 192 deletions

View File

@@ -31,138 +31,192 @@ class MailStore:
if not os.path.exists(self.storage_path):
os.makedirs(self.storage_path)
async def save_email(self, sender, recipient, message, raw_data):
async def save_email(self, message, sender, recipients, raw_data=None):
"""
保存一封电子邮件
保存邮件到数据库
参数:
sender: 发件人地址
recipient: 件人地址
message: 解析后的邮件对象
Args:
message: 已解析的邮件对象
sender: 件人邮箱
recipients: 收件人邮箱列表
raw_data: 原始邮件数据
返回:
成功返回邮件ID失败返回None
Returns:
(bool, str): 成功标志和错误信息
"""
# 从收件人地址中提取用户名和域名
try:
address, domain_name = recipient.split('@', 1)
except ValueError:
logger.warning(f"无效的收件人地址格式: {recipient}")
return None
logging.info(f"开始保存邮件: 发件人={sender}, 收件人={recipients}")
# 获取数据库会话
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
# 从消息对象中提取主题
subject = message.get('Subject', '')
if subject is None:
subject = ''
# 查找或创建邮箱
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', '')
logging.info(f"邮件主题: {subject}")
# 获取文本和HTML内容
body_text = None
body_html = None
attachments_data = []
# 获取邮件内容(文本和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 = 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()
content_disposition = str(part.get("Content-Disposition") or "")
logging.debug(f"处理邮件部分: 类型={content_type}, 处置={content_disposition}")
# 处理附件
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)
})
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()
if content_type == 'text/plain':
body_text = message.get_content()
elif content_type == 'text/html':
body_html = message.get_content()
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)}")
# 创建邮件记录
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()}
)
# 如果仍然没有内容,尝试从原始数据中提取
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)}")
# 保存邮件
db.add(email_obj)
db.flush() # 获取ID但不提交
logging.info(f"提取完成: 纯文本={len(body_text)}字节, HTML={len(body_html)}字节, 附件数={len(attachments)}")
# 提取验证信息
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']
# 保存到数据库
session = self.db_session_factory()
try:
# 创建新邮件记录
new_email = Email(
subject=subject,
sender=sender,
recipients=','.join(recipients) if isinstance(recipients, list) else recipients,
body_text=body_text,
body_html=body_html,
received_at=datetime.datetime.now()
)
db.add(attachment)
db.flush()
# 提取验证码和验证链接(如果有)
new_email.extract_verification_data()
# 决定存储位置
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
# 保存邮件
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:
db.rollback()
logger.error(f"保存邮件时出错: {str(e)}")
return None
finally:
db.close()
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):
"""获取指定邮箱的邮件列表"""
@@ -201,7 +255,7 @@ class MailStore:
if mark_as_read and not email.read:
email.read = True
email.last_read = datetime.utcnow()
email.last_read = datetime.datetime.now()
db.commit()
# 获取附件信息

View File

@@ -9,9 +9,14 @@ from aiosmtpd.handlers import Message
import os
import sys
import threading
import traceback
from ..models.domain import Domain
from ..models.mailbox import Mailbox
from ..utils import email_parser
from ..models import Email
from aiosmtpd.smtp import SMTP, Session, Envelope
logger = logging.getLogger(__name__)
@@ -34,29 +39,43 @@ class EmailHandler(Message):
async def handle_DATA(self, server, session, envelope):
"""处理接收到的邮件数据"""
try:
# 获取收件人和发件人
peer = session.peer
mail_from = envelope.mail_from
rcpt_tos = envelope.rcpt_tos
logging.info(f"收到邮件: 发件人={envelope.mail_from}, 收件人={envelope.rcpt_tos}")
# 获取原始邮件内容
data = envelope.content
mail = email.message_from_bytes(data, policy=default)
# 保存原始邮件数据
data = envelope.content.decode('utf-8', errors='replace')
# 保存邮件到存储服务
for rcpt in rcpt_tos:
result = await self.mail_store.save_email(mail_from, rcpt, mail, data)
# 解析邮件数据
message = email_parser.Parser().parsestr(data)
subject = message.get('Subject', '')
logging.info(f"邮件主题: {subject}")
# 记录邮件结构和内容
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}")
# 使用邮件存储服务保存邮件
success, error_msg = await self.mail_store.save_email(
message,
envelope.mail_from,
envelope.rcpt_tos,
raw_data=data
)
if success:
logging.info(f"邮件保存成功: 来自 {envelope.mail_from} 发送给 {envelope.rcpt_tos}")
return '250 消息接收完成'
else:
logging.error(f"邮件保存失败: {error_msg}")
return '451 处理邮件时出现错误,请稍后重试'
# 记录日志
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'
logging.error(f"处理邮件时出错: {str(e)}")
traceback.print_exc()
return '451 处理邮件时出现错误,请稍后重试'
# 为Windows环境自定义SMTP控制器
@@ -133,4 +152,69 @@ class SMTPServer:
return True
except Exception as e:
logger.error(f"停止SMTP服务器失败: {str(e)}")
return False
return False
class MailHandler:
"""邮件处理器用于处理接收的SMTP邮件"""
def __init__(self, mail_store):
self.mail_store = mail_store
async def handle_EHLO(self, server, session, envelope, hostname):
session.host_name = hostname
return '250-AUTH PLAIN\n250-SIZE 52428800\n250 SMTPUTF8'
async def handle_MAIL(self, server, session, envelope, address, mail_options=None):
if not mail_options:
mail_options = []
envelope.mail_from = address
envelope.mail_options.extend(mail_options)
return '250 OK'
async def handle_RCPT(self, server, session, envelope, address, rcpt_options=None):
if not rcpt_options:
rcpt_options = []
envelope.rcpt_tos.append(address)
envelope.rcpt_options.extend(rcpt_options)
return '250 OK'
async def handle_DATA(self, server, session, envelope):
"""处理接收到的邮件数据"""
try:
logging.info(f"收到邮件: 发件人={envelope.mail_from}, 收件人={envelope.rcpt_tos}")
# 保存原始邮件数据
raw_data = envelope.content.decode('utf-8', errors='replace')
# 解析邮件数据
message = email_parser.parsestr(raw_data)
subject = message.get('Subject', '')
logging.info(f"邮件主题: {subject}")
# 记录邮件结构和内容
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}")
# 使用邮件存储服务保存邮件
success, error_msg = await self.mail_store.save_email(
message,
envelope.mail_from,
envelope.rcpt_tos,
raw_data=raw_data
)
if success:
logging.info(f"邮件保存成功: 来自 {envelope.mail_from} 发送给 {envelope.rcpt_tos}")
return '250 消息接收完成'
else:
logging.error(f"邮件保存失败: {error_msg}")
return '451 处理邮件时出现错误,请稍后重试'
except Exception as e:
logging.error(f"处理邮件时出错: {str(e)}")
traceback.print_exc()
return '451 处理邮件时出现错误,请稍后重试'