2025-02-26 18:29:10 +08:00
|
|
|
|
import smtplib
|
|
|
|
|
|
import os
|
|
|
|
|
|
import json
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from email.parser import BytesParser
|
|
|
|
|
|
from email.policy import default
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from .models import db, Email
|
|
|
|
|
|
import redis
|
|
|
|
|
|
import smtpd
|
|
|
|
|
|
import asyncore
|
|
|
|
|
|
import base64
|
|
|
|
|
|
from .config import Config
|
|
|
|
|
|
|
|
|
|
|
|
# 配置日志
|
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
|
level=logging.INFO,
|
|
|
|
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
|
|
|
|
)
|
|
|
|
|
|
logger = logging.getLogger('smtp_server')
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化 Redis 客户端
|
|
|
|
|
|
redis_client = redis.from_url(Config.REDIS_URL)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def receive_email():
|
|
|
|
|
|
# 这里实现邮件接收逻辑
|
|
|
|
|
|
# 假设我们从某个 SMTP 服务器接收邮件
|
|
|
|
|
|
# 解析邮件并存储到 Redis
|
|
|
|
|
|
# 示例:
|
|
|
|
|
|
raw_email = b'...' # 这里应该是接收到的原始邮件内容
|
|
|
|
|
|
email = BytesParser(policy=default).parsebytes(raw_email)
|
|
|
|
|
|
email_data = {
|
|
|
|
|
|
'subject': email['subject'],
|
|
|
|
|
|
'sender': email['from'],
|
|
|
|
|
|
'recipient': email['to'],
|
|
|
|
|
|
'body': email.get_body(preferencelist=('plain')).get_content()
|
|
|
|
|
|
}
|
|
|
|
|
|
# 将邮件信息存储到 Redis
|
|
|
|
|
|
redis_client.hmset(f'email:{email_data['subject']}', email_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CustomSMTPServer(smtpd.SMTPServer):
|
|
|
|
|
|
def process_message(self, peer, mailfrom, rcpttos, data):
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 记录接收到的邮件基本信息
|
2025-02-26 18:38:10 +08:00
|
|
|
|
logger.info(f"Received mail from {mailfrom} to {rcpttos}")
|
2025-02-26 18:29:10 +08:00
|
|
|
|
|
|
|
|
|
|
# 验证收件人域名
|
|
|
|
|
|
for rcpt in rcpttos:
|
|
|
|
|
|
if not rcpt.endswith('@nosqli.com'):
|
2025-02-26 18:38:10 +08:00
|
|
|
|
logger.warning(f"Rejected mail to {rcpt}: invalid domain")
|
2025-02-26 18:29:10 +08:00
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 解析邮件
|
|
|
|
|
|
email = BytesParser(policy=default).parsebytes(data)
|
|
|
|
|
|
|
|
|
|
|
|
# 获取邮件正文
|
|
|
|
|
|
body = self._get_email_body(email)
|
|
|
|
|
|
|
|
|
|
|
|
# 处理附件
|
|
|
|
|
|
attachments = self._process_attachments(email)
|
|
|
|
|
|
|
|
|
|
|
|
# 构建邮件数据
|
|
|
|
|
|
timestamp = datetime.now().isoformat()
|
2025-02-26 18:38:10 +08:00
|
|
|
|
message_id = email.get('Message-ID', f"<{timestamp}@nosqli.com>")
|
2025-02-26 18:29:10 +08:00
|
|
|
|
|
|
|
|
|
|
email_data = {
|
|
|
|
|
|
'message_id': message_id,
|
|
|
|
|
|
'subject': email.get('subject', ''),
|
|
|
|
|
|
'sender': mailfrom,
|
|
|
|
|
|
'recipients': json.dumps(rcpttos),
|
|
|
|
|
|
'body': body,
|
|
|
|
|
|
'timestamp': timestamp,
|
|
|
|
|
|
'attachments': json.dumps(attachments),
|
|
|
|
|
|
'headers': json.dumps(dict(email.items())),
|
|
|
|
|
|
'peer': json.dumps(peer)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 存储邮件
|
|
|
|
|
|
self._store_email(email_data)
|
|
|
|
|
|
|
2025-02-26 18:38:10 +08:00
|
|
|
|
logger.info(f"Successfully processed mail: {message_id}")
|
2025-02-26 18:29:10 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-02-26 18:38:10 +08:00
|
|
|
|
logger.error(f"Error processing email: {str(e)}", exc_info=True)
|
2025-02-26 18:29:10 +08:00
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def _get_email_body(self, email):
|
|
|
|
|
|
"""提取邮件正文"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if email.is_multipart():
|
|
|
|
|
|
for part in email.walk():
|
|
|
|
|
|
if part.get_content_type() == "text/plain":
|
|
|
|
|
|
return part.get_payload(decode=True).decode()
|
|
|
|
|
|
else:
|
|
|
|
|
|
return email.get_payload(decode=True).decode()
|
|
|
|
|
|
return ""
|
|
|
|
|
|
except Exception as e:
|
2025-02-26 18:38:10 +08:00
|
|
|
|
logger.error(f"Error extracting email body: {str(e)}")
|
2025-02-26 18:29:10 +08:00
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
def _process_attachments(self, email):
|
|
|
|
|
|
"""处理邮件附件"""
|
|
|
|
|
|
attachments = []
|
|
|
|
|
|
try:
|
|
|
|
|
|
if email.is_multipart():
|
|
|
|
|
|
for part in email.walk():
|
|
|
|
|
|
if part.get_content_maintype() == 'multipart':
|
|
|
|
|
|
continue
|
|
|
|
|
|
if part.get('Content-Disposition') is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
filename = part.get_filename()
|
|
|
|
|
|
if filename:
|
|
|
|
|
|
attachment_data = part.get_payload(decode=True)
|
|
|
|
|
|
attachments.append({
|
|
|
|
|
|
'filename': filename,
|
|
|
|
|
|
'content': base64.b64encode(attachment_data).decode(),
|
|
|
|
|
|
'content_type': part.get_content_type(),
|
|
|
|
|
|
'size': len(attachment_data)
|
|
|
|
|
|
})
|
|
|
|
|
|
except Exception as e:
|
2025-02-26 18:38:10 +08:00
|
|
|
|
logger.error(f"Error processing attachments: {str(e)}")
|
2025-02-26 18:29:10 +08:00
|
|
|
|
return attachments
|
|
|
|
|
|
|
|
|
|
|
|
def _store_email(self, email_data):
|
|
|
|
|
|
"""存储邮件到 Redis"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 使用 message_id 作为主键
|
2025-02-26 18:38:10 +08:00
|
|
|
|
email_key = f"email:{email_data['message_id']}"
|
2025-02-26 18:29:10 +08:00
|
|
|
|
redis_client.hmset(email_key, email_data)
|
|
|
|
|
|
|
|
|
|
|
|
# 为每个收件人创建索引
|
|
|
|
|
|
recipients = json.loads(email_data['recipients'])
|
|
|
|
|
|
for recipient in recipients:
|
2025-02-26 18:38:10 +08:00
|
|
|
|
recipient_key = f"recipient:{recipient}"
|
2025-02-26 18:29:10 +08:00
|
|
|
|
redis_client.lpush(recipient_key, email_key)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建时间索引
|
2025-02-26 18:38:10 +08:00
|
|
|
|
time_key = f"time:{email_data['timestamp']}"
|
2025-02-26 18:29:10 +08:00
|
|
|
|
redis_client.set(time_key, email_key)
|
|
|
|
|
|
|
|
|
|
|
|
# 设置过期时间(可选,这里设置为30天)
|
|
|
|
|
|
redis_client.expire(email_key, 30 * 24 * 60 * 60)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-02-26 18:38:10 +08:00
|
|
|
|
logger.error(f"Error storing email: {str(e)}")
|
2025-02-26 18:29:10 +08:00
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def start_smtp_server(host='0.0.0.0', port=25):
|
|
|
|
|
|
"""启动 SMTP 服务器"""
|
|
|
|
|
|
try:
|
2025-02-26 18:38:10 +08:00
|
|
|
|
logger.info(f"Starting SMTP server on {host}:{port}")
|
2025-02-26 18:29:10 +08:00
|
|
|
|
server = CustomSMTPServer((host, port), None)
|
|
|
|
|
|
asyncore.loop()
|
|
|
|
|
|
except Exception as e:
|
2025-02-26 18:38:10 +08:00
|
|
|
|
logger.error(f"Error starting SMTP server: {str(e)}")
|
2025-02-26 18:29:10 +08:00
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_emails_by_recipient(recipient, limit=10):
|
|
|
|
|
|
"""获取指定收件人的最新邮件"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
recipient_key = f'recipient:{recipient}'
|
|
|
|
|
|
email_keys = redis_client.lrange(recipient_key, 0, limit - 1)
|
|
|
|
|
|
|
|
|
|
|
|
emails = []
|
|
|
|
|
|
for key in email_keys:
|
|
|
|
|
|
email_data = redis_client.hgetall(key.decode())
|
|
|
|
|
|
if email_data:
|
|
|
|
|
|
# 转换数据为字符串
|
|
|
|
|
|
email_data = {k.decode(): v.decode() for k, v in email_data.items()}
|
|
|
|
|
|
emails.append(email_data)
|
|
|
|
|
|
|
|
|
|
|
|
return emails
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f'Error fetching emails: {e}')
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_attachment(email_key, attachment_index):
|
|
|
|
|
|
"""获取指定邮件的附件"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
email_data = redis_client.hgetall(email_key)
|
|
|
|
|
|
if email_data:
|
|
|
|
|
|
attachments = json.loads(email_data[b'attachments'].decode())
|
|
|
|
|
|
if 0 <= attachment_index < len(attachments):
|
|
|
|
|
|
return attachments[attachment_index]
|
|
|
|
|
|
return None
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f'Error fetching attachment: {e}')
|
|
|
|
|
|
return None
|