203 lines
9.6 KiB
Python
203 lines
9.6 KiB
Python
import os
|
|
import shutil
|
|
from config import Config
|
|
from logger import logger
|
|
from temp_manager import TempManager
|
|
from mail_handler import MailHandler
|
|
from manifest_parser import ManifestParser
|
|
from image_processor import ImageProcessor
|
|
from zip_handler import ZipHandler
|
|
from agreement import AgreementManager
|
|
|
|
class MailConverter:
|
|
def __init__(self):
|
|
self.temp_mgr = TempManager()
|
|
self.mail = MailHandler()
|
|
self.img_proc = ImageProcessor()
|
|
self.zip_proc = ZipHandler()
|
|
|
|
def run(self):
|
|
# 清理遗留临时文件
|
|
TempManager.cleanup_stale()
|
|
# 检查协议同意
|
|
if not AgreementManager.is_agreed():
|
|
logger.info("首次使用,需要用户同意协议")
|
|
# 发送协议请求给管理员(或发件人?通常发给MAIL_USER)
|
|
recipient, subject, body = AgreementManager.request_agreement(Config.ADMIN_EMAIL, self.mail)
|
|
self.mail.send_result(recipient, subject, []) # 发送纯文本邮件
|
|
# 但这里我们还需监听回复?为简化,要求用户在.env中设置AGREED_TOS=True,或程序检测特定回复邮件
|
|
# 简单起见,输出日志提示管理员手动确认
|
|
logger.info(f"已向 {recipient} 发送协议同意请求,请在 .env 中设置 AGREED_TOS=True 后重新运行")
|
|
return
|
|
logger.info("协议已同意,开始处理邮件")
|
|
|
|
# 初始化临时会话目录
|
|
session_dir = self.temp_mgr.init_session()
|
|
# 获取未读邮件
|
|
emails = self.mail.fetch_unread_emails()
|
|
if not emails:
|
|
logger.info("没有待处理邮件")
|
|
self.temp_mgr.cleanup()
|
|
return
|
|
|
|
for msg_id, raw_email in emails:
|
|
# 解析发件人
|
|
msg = email.message_from_bytes(raw_email)
|
|
sender = msg.get('From')
|
|
sender_email = parseaddr(sender)[1]
|
|
if not self.mail.is_domain_allowed(sender_email):
|
|
logger.warning(f"域名不在白名单: {sender_email},跳过处理")
|
|
continue
|
|
|
|
# 下载附件和正文
|
|
attachments, body = self.mail.download_attachments(raw_email, session_dir)
|
|
# 合并 manifest.txt 内容
|
|
manifest_path = None
|
|
for att in attachments:
|
|
if os.path.basename(att) == 'manifest.txt':
|
|
manifest_path = att
|
|
break
|
|
if manifest_path:
|
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
|
rule_content = f.read()
|
|
else:
|
|
rule_content = body # 使用邮件正文
|
|
|
|
if not rule_content.strip():
|
|
logger.warning("无转换规则,跳过")
|
|
continue
|
|
|
|
# 解析规则
|
|
parser = ManifestParser()
|
|
tasks = parser.parse(rule_content)
|
|
|
|
# 处理每个附件(图片或压缩包)
|
|
result_items = [] # 每个元素为 (output_path_or_dir, original_attachment_name)
|
|
for att in attachments:
|
|
if os.path.basename(att) == 'manifest.txt':
|
|
continue
|
|
if ZipHandler.is_archive(att):
|
|
# 处理压缩包
|
|
output_dir = self.process_archive(att, tasks, session_dir)
|
|
if output_dir:
|
|
# 压缩包处理结果可能是一个目录(内含多个图片结果),或者单个文件?
|
|
# 为保持原样式,如果输出目录内只有一个文件,则返回该文件;否则打包目录
|
|
result_items.append((output_dir, os.path.basename(att)))
|
|
else:
|
|
# 普通图片附件
|
|
output_dir = self.process_single_image(att, tasks, session_dir)
|
|
if output_dir:
|
|
result_items.append((output_dir, os.path.basename(att)))
|
|
|
|
# 构建返回附件列表(根据原样式规则)
|
|
return_attachments = []
|
|
for output, original_name in result_items:
|
|
if Config.FLATTEN_OUTPUT:
|
|
# 平铺所有文件
|
|
if os.path.isdir(output):
|
|
for f in os.listdir(output):
|
|
return_attachments.append(os.path.join(output, f))
|
|
else:
|
|
return_attachments.append(output)
|
|
else:
|
|
# 保持原附件数量:如果输出是一个目录,且目录内文件数==1,则直接返回该文件;否则打包为ZIP
|
|
if os.path.isdir(output):
|
|
files = [f for f in os.listdir(output) if os.path.isfile(os.path.join(output, f))]
|
|
if len(files) == 1:
|
|
return_attachments.append(os.path.join(output, files[0]))
|
|
else:
|
|
# 打包目录
|
|
zip_path = os.path.join(session_dir, f"{os.path.splitext(original_name)[0]}_result.zip")
|
|
ZipHandler.pack_to_zip(output, zip_path)
|
|
return_attachments.append(zip_path)
|
|
else:
|
|
return_attachments.append(output)
|
|
|
|
# 发送结果
|
|
subject = f"转换结果: {msg.get('Subject', '无主题')}"
|
|
try:
|
|
self.mail.send_result(sender_email, subject, return_attachments, Config.SPLIT_VOLUME_SIZE_MB)
|
|
# 删除原邮件(留痕已在日志中)
|
|
# 对于IMAP,我们可以删除原邮件
|
|
# 注意:需要重新连接,因为之前的连接已关闭。简单起见,不在这里实现删除,留痕已足够
|
|
# 留痕记录
|
|
logger.info(f"成功处理邮件 from {sender_email}, msg_id={msg_id}")
|
|
except Exception as e:
|
|
error_msg = f"发送结果失败: {str(e)}"
|
|
logger.error(error_msg)
|
|
self.mail.send_error_report(sender_email, error_msg)
|
|
|
|
# 清理会话临时文件(若配置)
|
|
if not Config.KEEP_TEMP_FILES:
|
|
self.temp_mgr.cleanup()
|
|
|
|
def process_single_image(self, img_path, tasks, base_dir):
|
|
"""处理单张图片,返回输出目录(存放所有转换结果)"""
|
|
# 根据tasks匹配规则,生成输出文件列表
|
|
output_dir = os.path.join(base_dir, f"out_{os.path.basename(img_path)}")
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
# 简化匹配逻辑:只实现全局规则和文件名匹配(实际可扩展)
|
|
# 此处需要完整实现优先级匹配,为简洁,示例只做基础
|
|
# 实际项目中应实现完整的规则引擎,这里模拟
|
|
filename = os.path.splitext(os.path.basename(img_path))[0]
|
|
# 找是否有匹配的from.name规则
|
|
matched = False
|
|
for task in tasks:
|
|
if task['type'] == 'by_name' and task['src_name'] == filename:
|
|
for fmt, size in task['targets']:
|
|
out_path = os.path.join(output_dir, f"{filename}.{fmt}")
|
|
# 缩放
|
|
img = Image.open(img_path)
|
|
if size:
|
|
if size[0] == 'ratio':
|
|
img = ImageProcessor.resize_by_ratio(img, size[1], size[2])
|
|
elif size[0] == 'pixel':
|
|
img = ImageProcessor.resize_by_pixel(img, size[1], size[2])
|
|
img.save(out_path, format=fmt.upper(), quality=Config.DEFAULT_QUALITY)
|
|
else:
|
|
ImageProcessor.convert_image(img_path, out_path, fmt)
|
|
matched = True
|
|
break
|
|
if not matched:
|
|
# 全局规则
|
|
for task in tasks:
|
|
if task['type'] == 'global':
|
|
for fmt, size in task['targets']:
|
|
out_path = os.path.join(output_dir, f"{filename}.{fmt}")
|
|
ImageProcessor.convert_image(img_path, out_path, fmt)
|
|
matched = True
|
|
break
|
|
if not matched:
|
|
# 无规则,原样复制
|
|
shutil.copy(img_path, os.path.join(output_dir, os.path.basename(img_path)))
|
|
return output_dir
|
|
|
|
def process_archive(self, archive_path, tasks, base_dir):
|
|
"""处理压缩包,返回输出目录"""
|
|
extract_dir = os.path.join(base_dir, f"ext_{os.path.basename(archive_path)}")
|
|
os.makedirs(extract_dir, exist_ok=True)
|
|
ext = os.path.splitext(archive_path)[1].lower()
|
|
if ext == '.zip':
|
|
ZipHandler.extract_zip(archive_path, extract_dir)
|
|
elif ext == '.7z':
|
|
ZipHandler.extract_7z(archive_path, extract_dir)
|
|
else:
|
|
return None
|
|
|
|
# 遍历提取出的所有图片,应用规则(规则作用域可能指定该压缩包)
|
|
output_root = os.path.join(base_dir, f"out_{os.path.basename(archive_path)}")
|
|
os.makedirs(output_root, exist_ok=True)
|
|
for root, _, files in os.walk(extract_dir):
|
|
for file in files:
|
|
if file.split('.')[-1].lower() in Config.SUPPORTED_INPUT_FORMATS:
|
|
img_path = os.path.join(root, file)
|
|
# 对每个图片应用规则(同上,简化)
|
|
out_subdir = self.process_single_image(img_path, tasks, output_root)
|
|
# 合并输出目录
|
|
for f in os.listdir(out_subdir):
|
|
shutil.move(os.path.join(out_subdir, f), output_root)
|
|
return output_root
|
|
|
|
if __name__ == "__main__":
|
|
converter = MailConverter()
|
|
converter.run() |