201 lines
8.1 KiB
Python
201 lines
8.1 KiB
Python
|
|
import imaplib
|
||
|
|
import poplib
|
||
|
|
import smtplib
|
||
|
|
import email
|
||
|
|
from email.mime.text import MIMEText
|
||
|
|
from email.mime.multipart import MIMEMultipart
|
||
|
|
from email.mime.base import MIMEBase
|
||
|
|
from email import encoders
|
||
|
|
import os
|
||
|
|
import re
|
||
|
|
from email.utils import parseaddr
|
||
|
|
from config import Config
|
||
|
|
from logger import logger
|
||
|
|
|
||
|
|
class MailHandler:
|
||
|
|
def __init__(self):
|
||
|
|
self.user = Config.MAIL_USER
|
||
|
|
self.password = Config.MAIL_PASS
|
||
|
|
self.smtp_server = Config.SMTP_SERVER
|
||
|
|
self.smtp_port = Config.SMTP_PORT
|
||
|
|
|
||
|
|
def connect_inbox(self):
|
||
|
|
"""根据协议返回收信连接对象"""
|
||
|
|
if Config.MAIL_PROTOCOL == "IMAP":
|
||
|
|
conn = imaplib.IMAP4_SSL(Config.IMAP_SERVER, Config.IMAP_PORT)
|
||
|
|
conn.login(self.user, self.password)
|
||
|
|
conn.select('INBOX')
|
||
|
|
return conn, "IMAP"
|
||
|
|
else:
|
||
|
|
conn = poplib.POP3_SSL(Config.POP3_SERVER, Config.POP3_PORT)
|
||
|
|
conn.user(self.user)
|
||
|
|
conn.pass_(self.password)
|
||
|
|
return conn, "POP3"
|
||
|
|
|
||
|
|
def fetch_unread_emails(self):
|
||
|
|
"""获取未读邮件列表,返回列表 [ (msg_id, raw_email) ]"""
|
||
|
|
conn, proto = self.connect_inbox()
|
||
|
|
emails = []
|
||
|
|
try:
|
||
|
|
if proto == "IMAP":
|
||
|
|
# 搜索未读邮件
|
||
|
|
typ, data = conn.search(None, 'UNSEEN')
|
||
|
|
if typ != 'OK':
|
||
|
|
logger.error("IMAP搜索未读邮件失败")
|
||
|
|
return []
|
||
|
|
for num in data[0].split():
|
||
|
|
typ, msg_data = conn.fetch(num, '(RFC822)')
|
||
|
|
if typ != 'OK':
|
||
|
|
continue
|
||
|
|
raw_email = msg_data[0][1]
|
||
|
|
emails.append((num, raw_email))
|
||
|
|
if len(emails) >= Config.MAX_EMAILS_PER_RUN:
|
||
|
|
break
|
||
|
|
else: # POP3
|
||
|
|
# POP3 不支持未读标志,获取所有邮件列表
|
||
|
|
msg_count = len(conn.list()[1])
|
||
|
|
for i in range(msg_count, 0, -1):
|
||
|
|
raw_email = b'\n'.join(conn.retr(i)[1])
|
||
|
|
emails.append((i, raw_email))
|
||
|
|
if len(emails) >= Config.MAX_EMAILS_PER_RUN:
|
||
|
|
break
|
||
|
|
logger.info(f"获取到 {len(emails)} 封待处理邮件")
|
||
|
|
return emails
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"收信失败: {e}")
|
||
|
|
return []
|
||
|
|
finally:
|
||
|
|
if proto == "IMAP":
|
||
|
|
conn.close()
|
||
|
|
conn.quit()
|
||
|
|
|
||
|
|
def mark_as_deleted(self, msg_id, proto_conn=None):
|
||
|
|
"""标记邮件为已删除(仅IMAP支持删除,POP3需额外实现)"""
|
||
|
|
if Config.MAIL_PROTOCOL == "IMAP" and proto_conn:
|
||
|
|
proto_conn.store(msg_id, '+FLAGS', '\\Deleted')
|
||
|
|
logger.info(f"标记邮件 {msg_id} 为已删除")
|
||
|
|
# POP3 删除需要重新连接并执行DELE,这里简化,留痕已足够
|
||
|
|
|
||
|
|
def is_domain_allowed(self, sender_email):
|
||
|
|
"""检查发件人域名是否在白名单中"""
|
||
|
|
if not Config.ALLOWED_DOMAINS:
|
||
|
|
return True
|
||
|
|
domain = sender_email.split('@')[-1].lower()
|
||
|
|
for allowed in Config.ALLOWED_DOMAINS:
|
||
|
|
if allowed.startswith('*.'):
|
||
|
|
# 子域名匹配
|
||
|
|
suffix = allowed[1:] # .example.com
|
||
|
|
if domain.endswith(suffix):
|
||
|
|
return True
|
||
|
|
elif domain == allowed.lower():
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
def download_attachments(self, raw_email, temp_dir):
|
||
|
|
"""解析邮件,下载附件(图片、压缩包、manifest.txt)到temp_dir,返回附件列表"""
|
||
|
|
msg = email.message_from_bytes(raw_email)
|
||
|
|
attachments = []
|
||
|
|
for part in msg.walk():
|
||
|
|
if part.get_content_maintype() == 'multipart':
|
||
|
|
continue
|
||
|
|
filename = part.get_filename()
|
||
|
|
if not filename:
|
||
|
|
continue
|
||
|
|
# 判断是否为支持的类型
|
||
|
|
ext = os.path.splitext(filename)[1].lower()
|
||
|
|
if ext in ['.txt'] and filename == 'manifest.txt':
|
||
|
|
pass
|
||
|
|
elif ext in ['.zip', '.7z']:
|
||
|
|
pass
|
||
|
|
elif ext[1:] in Config.SUPPORTED_INPUT_FORMATS:
|
||
|
|
pass
|
||
|
|
else:
|
||
|
|
continue
|
||
|
|
filepath = os.path.join(temp_dir, filename)
|
||
|
|
with open(filepath, 'wb') as f:
|
||
|
|
f.write(part.get_payload(decode=True))
|
||
|
|
attachments.append(filepath)
|
||
|
|
# 提取邮件正文作为指令备用
|
||
|
|
body = ""
|
||
|
|
if msg.is_multipart():
|
||
|
|
for part in msg.walk():
|
||
|
|
if part.get_content_type() == 'text/plain':
|
||
|
|
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
|
||
|
|
break
|
||
|
|
else:
|
||
|
|
body = msg.get_payload(decode=True).decode('utf-8', errors='ignore')
|
||
|
|
return attachments, body
|
||
|
|
|
||
|
|
def send_result(self, recipient, subject, attachments, split_volume_mb=0):
|
||
|
|
"""发送结果邮件,支持分卷附件"""
|
||
|
|
msg = MIMEMultipart()
|
||
|
|
msg['From'] = self.user
|
||
|
|
msg['To'] = recipient
|
||
|
|
msg['Subject'] = subject
|
||
|
|
text = MIMEText("图片转换完成,请查收附件。", 'plain', 'utf-8')
|
||
|
|
msg.attach(text)
|
||
|
|
|
||
|
|
# 处理分卷
|
||
|
|
final_attachments = []
|
||
|
|
for att in attachments:
|
||
|
|
if os.path.isdir(att):
|
||
|
|
# 如果是目录,打包成ZIP
|
||
|
|
zip_path = att + ".zip"
|
||
|
|
from zip_handler import ZipHandler
|
||
|
|
ZipHandler.pack_to_zip(att, zip_path)
|
||
|
|
att = zip_path
|
||
|
|
if split_volume_mb > 0 and os.path.getsize(att) > split_volume_mb * 1024 * 1024:
|
||
|
|
from zip_handler import ZipHandler
|
||
|
|
parts = ZipHandler.split_zip(att, split_volume_mb)
|
||
|
|
final_attachments.extend(parts)
|
||
|
|
else:
|
||
|
|
final_attachments.append(att)
|
||
|
|
|
||
|
|
# 检查每个附件大小是否超过SMTP限制
|
||
|
|
max_bytes = Config.MAX_ATTACHMENT_SIZE_MB * 1024 * 1024
|
||
|
|
for att in final_attachments:
|
||
|
|
if os.path.getsize(att) > max_bytes:
|
||
|
|
logger.warning(f"附件 {att} 超过限制,跳过发送")
|
||
|
|
continue
|
||
|
|
with open(att, 'rb') as f:
|
||
|
|
part = MIMEBase('application', 'octet-stream')
|
||
|
|
part.set_payload(f.read())
|
||
|
|
encoders.encode_base64(part)
|
||
|
|
part.add_header('Content-Disposition', f'attachment; filename="{os.path.basename(att)}"')
|
||
|
|
msg.attach(part)
|
||
|
|
|
||
|
|
try:
|
||
|
|
if Config.SMTP_PORT == 465:
|
||
|
|
with smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) as server:
|
||
|
|
server.login(self.user, self.password)
|
||
|
|
server.send_message(msg)
|
||
|
|
else:
|
||
|
|
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
|
||
|
|
server.starttls()
|
||
|
|
server.login(self.user, self.password)
|
||
|
|
server.send_message(msg)
|
||
|
|
logger.info(f"结果邮件已发送至 {recipient}")
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"发送失败: {e}")
|
||
|
|
raise
|
||
|
|
|
||
|
|
def send_error_report(self, recipient, error_msg):
|
||
|
|
"""发送脱敏后的错误报告"""
|
||
|
|
subject = "图片转换工具 - 处理出错"
|
||
|
|
body = f"处理您的邮件时发生错误,错误信息(已脱敏):\n\n{error_msg}\n\n请检查指令格式或图片格式。"
|
||
|
|
msg = MIMEText(body, 'plain', 'utf-8')
|
||
|
|
msg['From'] = self.user
|
||
|
|
msg['To'] = recipient
|
||
|
|
msg['Subject'] = subject
|
||
|
|
try:
|
||
|
|
if Config.SMTP_PORT == 465:
|
||
|
|
with smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) as server:
|
||
|
|
server.login(self.user, self.password)
|
||
|
|
server.send_message(msg)
|
||
|
|
else:
|
||
|
|
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
|
||
|
|
server.starttls()
|
||
|
|
server.login(self.user, self.password)
|
||
|
|
server.send_message(msg)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"错误报告发送失败: {e}")
|