init
This commit is contained in:
+201
@@ -0,0 +1,201 @@
|
||||
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}")
|
||||
Reference in New Issue
Block a user