增强:添加批量注册功能和支持文件
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
from flask import request, jsonify, current_app, send_file
|
||||
from io import BytesIO
|
||||
import time
|
||||
import os
|
||||
import logging
|
||||
|
||||
from . import api_bp
|
||||
from ..models import get_session, Email, Mailbox
|
||||
from ..models import get_session, Email, Mailbox, Domain
|
||||
from ..utils import email_parser
|
||||
from ..config import config
|
||||
|
||||
# 获取邮箱的所有邮件
|
||||
@api_bp.route('/mailboxes/<int:mailbox_id>/emails', methods=['GET'])
|
||||
@@ -39,6 +43,7 @@ def get_mailbox_emails(mailbox_id):
|
||||
|
||||
# 返回结果
|
||||
result = {
|
||||
'success': True,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
@@ -50,49 +55,122 @@ def get_mailbox_emails(mailbox_id):
|
||||
db.close()
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"获取邮件列表出错: {str(e)}")
|
||||
return jsonify({'error': '获取邮件列表失败', 'details': str(e)}), 500
|
||||
return jsonify({'success': False, 'error': '获取邮件列表失败', 'details': str(e)}), 500
|
||||
|
||||
# 获取特定邮件详情
|
||||
@api_bp.route('/emails/<int:email_id>', methods=['GET'])
|
||||
def get_email(email_id):
|
||||
"""获取特定邮件的详细信息"""
|
||||
"""
|
||||
获取单个邮件的详细信息
|
||||
"""
|
||||
try:
|
||||
mark_as_read = request.args.get('mark_as_read', 'true').lower() == 'true'
|
||||
email_id = int(email_id)
|
||||
session = get_session()
|
||||
email = session.query(Email).filter(Email.id == email_id).first()
|
||||
|
||||
if not email:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'未找到ID为{email_id}的邮件'
|
||||
}), 404
|
||||
|
||||
# 获取邮件正文内容
|
||||
body_text = None
|
||||
body_html = None
|
||||
|
||||
db = get_session()
|
||||
try:
|
||||
email = db.query(Email).filter_by(id=email_id).first()
|
||||
# 尝试从文件中读取邮件内容
|
||||
if email.id:
|
||||
email_path = os.path.join(config.DATA_DIR, 'emails', f'email_{email.id}.eml')
|
||||
if os.path.exists(email_path):
|
||||
logging.info(f"从文件读取邮件内容: {email_path}")
|
||||
with open(email_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
try:
|
||||
raw_email = f.read()
|
||||
msg = email_parser.parsestr(raw_email)
|
||||
|
||||
if msg.is_multipart():
|
||||
# 处理多部分邮件
|
||||
for part in msg.walk():
|
||||
content_type = part.get_content_type()
|
||||
content_disposition = str(part.get("Content-Disposition"))
|
||||
|
||||
# 跳过附件
|
||||
if "attachment" in content_disposition:
|
||||
continue
|
||||
|
||||
# 处理文本内容
|
||||
if content_type == "text/plain":
|
||||
payload = part.get_payload(decode=True)
|
||||
charset = part.get_content_charset() or 'utf-8'
|
||||
try:
|
||||
body_text = payload.decode(charset, errors='replace')
|
||||
except Exception as e:
|
||||
logging.error(f"解码纯文本内容失败: {e}")
|
||||
body_text = payload.decode('utf-8', errors='replace')
|
||||
|
||||
# 处理HTML内容
|
||||
elif content_type == "text/html":
|
||||
payload = part.get_payload(decode=True)
|
||||
charset = part.get_content_charset() or 'utf-8'
|
||||
try:
|
||||
body_html = payload.decode(charset, errors='replace')
|
||||
except Exception as e:
|
||||
logging.error(f"解码HTML内容失败: {e}")
|
||||
body_html = payload.decode('utf-8', errors='replace')
|
||||
else:
|
||||
# 处理单部分邮件
|
||||
content_type = msg.get_content_type()
|
||||
payload = msg.get_payload(decode=True)
|
||||
charset = msg.get_content_charset() or 'utf-8'
|
||||
|
||||
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
|
||||
elif content_type == "text/html":
|
||||
body_html = decoded_content
|
||||
except Exception as e:
|
||||
logging.error(f"解析邮件文件失败: {e}")
|
||||
except Exception as e:
|
||||
logging.error(f"读取邮件内容时出错: {e}")
|
||||
|
||||
# 如果文件读取失败,使用数据库中的内容
|
||||
if body_text is None:
|
||||
body_text = email.body_text
|
||||
|
||||
if body_html is None:
|
||||
body_html = email.body_html
|
||||
|
||||
if not email:
|
||||
return jsonify({'error': '邮件不存在', 'success': False}), 404
|
||||
|
||||
# 标记为已读
|
||||
if mark_as_read and not email.read:
|
||||
email.read = True
|
||||
db.commit()
|
||||
|
||||
# 构建详细响应
|
||||
result = email.to_dict()
|
||||
result['body_text'] = email.body_text
|
||||
result['body_html'] = email.body_html
|
||||
|
||||
# 获取附件信息
|
||||
attachments = []
|
||||
for attachment in email.attachments:
|
||||
attachments.append({
|
||||
'id': attachment.id,
|
||||
'filename': attachment.filename,
|
||||
'content_type': attachment.content_type,
|
||||
'size': attachment.size
|
||||
})
|
||||
result['attachments'] = attachments
|
||||
|
||||
return jsonify({'email': result, 'success': True}), 200
|
||||
finally:
|
||||
db.close()
|
||||
logging.info(f"邮件ID={email_id} 正文长度: text={len(body_text or '')}字节, html={len(body_html or '')}字节")
|
||||
|
||||
# 返回邮件信息,包括正文内容
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'email': {
|
||||
'id': email.id,
|
||||
'subject': email.subject,
|
||||
'sender': email.sender,
|
||||
'recipients': email.recipients,
|
||||
'received_at': email.received_at.isoformat(),
|
||||
'verification_code': email.verification_code,
|
||||
'verification_link': email.verification_link,
|
||||
'body_text': body_text,
|
||||
'body_html': body_html
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"获取邮件详情出错: {str(e)}")
|
||||
return jsonify({'error': '获取邮件详情失败', 'details': str(e), 'success': False}), 500
|
||||
logging.error(f"获取邮件时出错: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'获取邮件时发生错误: {str(e)}'
|
||||
}), 500
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# 删除邮件
|
||||
@api_bp.route('/emails/<int:email_id>', methods=['DELETE'])
|
||||
@@ -195,4 +273,110 @@ def poll_new_emails(mailbox_id):
|
||||
db.close()
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"轮询新邮件出错: {str(e)}")
|
||||
return jsonify({'error': '轮询新邮件失败', 'details': str(e)}), 500
|
||||
return jsonify({'error': '轮询新邮件失败', 'details': str(e)}), 500
|
||||
|
||||
# 通过邮箱地址获取最新邮件
|
||||
@api_bp.route('/emails/by-address', methods=['GET'])
|
||||
def get_emails_by_address():
|
||||
"""
|
||||
通过邮箱地址获取最新邮件
|
||||
|
||||
参数:
|
||||
email_address: 完整邮箱地址 (例如: user@example.com)
|
||||
limit: 返回的邮件数量 (默认: 10)
|
||||
since: 从指定时间戳后获取邮件 (可选)
|
||||
unread_only: 是否只返回未读邮件 (默认: false)
|
||||
|
||||
返回:
|
||||
最新的邮件列表
|
||||
"""
|
||||
try:
|
||||
email_address = request.args.get('email_address')
|
||||
if not email_address or '@' not in email_address:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '请提供有效的邮箱地址 (格式: user@example.com)'
|
||||
}), 400
|
||||
|
||||
limit = int(request.args.get('limit', 10))
|
||||
unread_only = request.args.get('unread_only', 'false').lower() == 'true'
|
||||
since = request.args.get('since')
|
||||
|
||||
# 解析邮箱地址
|
||||
try:
|
||||
username, domain_name = email_address.split('@', 1)
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '邮箱地址格式无效'
|
||||
}), 400
|
||||
|
||||
db = get_session()
|
||||
try:
|
||||
# 查找域名
|
||||
domain = db.query(Domain).filter_by(name=domain_name, active=True).first()
|
||||
if not domain:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'域名 {domain_name} 不存在或未激活'
|
||||
}), 404
|
||||
|
||||
# 查找邮箱
|
||||
mailbox = db.query(Mailbox).filter_by(address=username, domain_id=domain.id).first()
|
||||
if not mailbox:
|
||||
# 自动创建邮箱 - 批量注册场景
|
||||
mailbox = Mailbox(
|
||||
address=username,
|
||||
domain_id=domain.id,
|
||||
description=f"自动创建 ({email_address})",
|
||||
active=True
|
||||
)
|
||||
db.add(mailbox)
|
||||
db.flush() # 获取新创建邮箱的ID
|
||||
logging.info(f"自动创建邮箱: {email_address}, ID={mailbox.id}")
|
||||
|
||||
# 查询邮件
|
||||
query = db.query(Email).filter(Email.mailbox_id == mailbox.id)
|
||||
|
||||
if unread_only:
|
||||
query = query.filter(Email.read == False)
|
||||
|
||||
if since:
|
||||
try:
|
||||
since_time = float(since)
|
||||
query = query.filter(
|
||||
Email.received_at >= time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(since_time))
|
||||
)
|
||||
except ValueError:
|
||||
logging.warning(f"无效的since参数: {since}")
|
||||
|
||||
# 获取最新的邮件
|
||||
emails = query.order_by(Email.received_at.desc()).limit(limit).all()
|
||||
|
||||
# 获取总数
|
||||
total = query.count()
|
||||
|
||||
# 提交数据库变更
|
||||
db.commit()
|
||||
|
||||
# 返回结果
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'email_address': email_address,
|
||||
'mailbox_id': mailbox.id,
|
||||
'total': total,
|
||||
'count': len(emails),
|
||||
'emails': [email.to_dict() for email in emails],
|
||||
'timestamp': time.time()
|
||||
}), 200
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logging.error(f"获取邮件出错: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'获取邮件失败: {str(e)}'
|
||||
}), 500
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import re
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from . import Base
|
||||
import config
|
||||
@@ -50,22 +51,58 @@ class Email(Base):
|
||||
尝试从邮件内容中提取验证码和验证链接
|
||||
这个方法会在邮件保存时自动调用
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 合并文本和HTML内容用于搜索
|
||||
content = f"{self.subject} {self.body_text or ''}"
|
||||
content = f"{self.subject or ''} {self.body_text or ''} {self.body_html or ''}"
|
||||
logger.info(f"开始提取邮件ID={self.id}的验证信息,内容长度={len(content)}")
|
||||
|
||||
# 首先检查是否是Cursor验证邮件
|
||||
if "Verify your email" in self.subject and (
|
||||
"cursor.sh" in self.sender.lower() or
|
||||
"cursor" in self.sender.lower()
|
||||
):
|
||||
logger.info("检测到Cursor验证邮件")
|
||||
# 针对Cursor验证邮件的特定验证码格式
|
||||
import re
|
||||
# 从HTML中提取6位数字验证码
|
||||
cursor_patterns = [
|
||||
r'(\d{6})</div>', # 匹配Cursor邮件中的6位数字验证码格式
|
||||
r'<div[^>]*>(\d{6})</div>', # 更宽松的匹配
|
||||
r'>(\d{6})<', # 最简单的形式
|
||||
r'(\d{6})' # 任何6位数字
|
||||
]
|
||||
|
||||
for pattern in cursor_patterns:
|
||||
matches = re.findall(pattern, content)
|
||||
if matches:
|
||||
self.verification_code = matches[0]
|
||||
logger.info(f"从Cursor邮件中提取到验证码: {self.verification_code}")
|
||||
break
|
||||
|
||||
return
|
||||
|
||||
# 提取可能的验证码(4-8位数字或字母组合)
|
||||
code_patterns = [
|
||||
r'\b[A-Z0-9]{4,8}\b', # 大写字母和数字
|
||||
r'\b([A-Z0-9]{4,8})\b', # 大写字母和数字
|
||||
r'验证码[::]\s*([A-Z0-9]{4,8})', # 中文格式
|
||||
r'验证码是[::]\s*([A-Z0-9]{4,8})', # 中文格式2
|
||||
r'code[::]\s*([A-Z0-9]{4,8})', # 英文格式
|
||||
r'code is[::]\s*([A-Z0-9]{4,8})', # 英文格式2
|
||||
r'code[::]\s*<[^>]*>([A-Z0-9]{4,8})', # HTML格式
|
||||
r'<div[^>]*>([0-9]{4,8})</div>', # HTML分隔的数字
|
||||
r'<strong[^>]*>([A-Z0-9]{4,8})</strong>', # 粗体验证码
|
||||
]
|
||||
|
||||
for pattern in code_patterns:
|
||||
matches = re.findall(pattern, content, re.IGNORECASE)
|
||||
if matches:
|
||||
self.verification_code = matches[0]
|
||||
break
|
||||
# 过滤掉明显不是验证码的结果
|
||||
filtered_matches = [m for m in matches if len(m) >= 4 and not m.lower() in ['code', 'verify', 'http', 'https']]
|
||||
if filtered_matches:
|
||||
self.verification_code = filtered_matches[0]
|
||||
logger.info(f"提取到验证码: {self.verification_code}")
|
||||
break
|
||||
|
||||
# 提取验证链接
|
||||
link_patterns = [
|
||||
@@ -77,7 +114,22 @@ class Email(Base):
|
||||
matches = re.findall(pattern, content, re.IGNORECASE)
|
||||
if matches:
|
||||
self.verification_link = matches[0]
|
||||
logger.info(f"提取到验证链接: {self.verification_link}")
|
||||
break
|
||||
|
||||
# 如果没有找到验证码,但邮件主题暗示这是验证邮件
|
||||
verify_subjects = ['verify', 'confirmation', 'activate', 'validation', '验证', '确认']
|
||||
if not self.verification_code and any(subj in self.subject.lower() for subj in verify_subjects):
|
||||
logger.info("根据主题判断这可能是验证邮件,但未能提取到验证码")
|
||||
# 尝试从HTML中提取明显的数字序列
|
||||
if self.body_html:
|
||||
number_matches = re.findall(r'(\d{4,8})', self.body_html)
|
||||
filtered_numbers = [n for n in number_matches if len(n) >= 4 and len(n) <= 8]
|
||||
if filtered_numbers:
|
||||
self.verification_code = filtered_numbers[0]
|
||||
logger.info(f"从HTML中提取到可能的验证码: {self.verification_code}")
|
||||
|
||||
logger.info(f"验证信息提取完成: code={self.verification_code}, link={self.verification_link}")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Email {self.id}: {self.subject}>"
|
||||
|
||||
@@ -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()
|
||||
|
||||
# 获取附件信息
|
||||
|
||||
@@ -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 处理邮件时出现错误,请稍后重试'
|
||||
9
app/utils/__init__.py
Normal file
9
app/utils/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
工具类和辅助函数
|
||||
"""
|
||||
import email.parser as email_parser
|
||||
import email.policy
|
||||
|
||||
# 创建邮件解析器实例,用于解析邮件
|
||||
parser = email_parser.Parser()
|
||||
parsestr = parser.parsestr
|
||||
Reference in New Issue
Block a user