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()