Files
mailc/mail_handler.py
T

201 lines
8.1 KiB
Python
Raw Normal View History

2026-04-18 23:04:03 +08:00
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}")