文章目录
一、介绍
这是一个用Python写的自动化工具,用于监控126邮箱中的未读邮件,自动提取其中的迅雷链接和磁力链接,打开迅雷客户端自动完成下载任务。如果你出门在外,看到一部大片的下载链接,马上发送一封电子邮件给自己,让Python帮你打开迅雷自动下载,回家后就可以直接观看了!
二、功能特点
- 自动检查126邮箱未读邮件
- 支持迅雷专有链接(thunder://)和磁力链接(magnet:)
- 自动启动迅雷并触发下载 - 智能窗口识别和按钮定位
- 详细的日志记录 - 定时检查功能(每小时)
- 异常自动恢复
三、运行界面
四、技术实现
1. 邮件处理
- 使用POP3_SSL协议连接126邮箱
- 支持多种未读邮件判定标记
- 自动解码邮件内容(支持多种编码)
- 正则表达式匹配下载链接
2. 窗口操作
- 使用win32gui进行窗口查找和操作
- 支持Chrome渲染窗口的识别
- 智能计算按钮位置
- 模拟鼠标点击操作
3. 自动化流程
1) 连接邮箱
2) 检查未读邮件
3) 提取下载链接
4) 启动迅雷
5) 等待下载窗口
6) 模拟点击下载
4. 创建配置文件
在程序根目录下新建txt文件输入以下内容,保存后改名为config.ini
[Email]
email_address = your_email@126.com
email_password = your_password
说明:126邮箱 - 需要使用授权码而不是登录密码 - 必须开启POP3服务
5. 迅雷配置
self.thunder_path = r"你自己电脑上迅雷可执行文件的绝对路径"
窗口识别规则
- 主窗口:标题="迅雷下载",类名="CabinetWClass"
- 下载任务窗口:标题包含["新建任务", "新建下载任务", "下载任务"]
- Chrome渲染窗口:类名="Chrome_RenderWidgetHostHWND"
五、导入库
import imaplib
import email
import time
import re
import os
import win32com.client
from datetime import datetime, timedelta
import logging
import configparser
import sys
import poplib
import win32gui
import win32con
import win32api
import win32ui
from ctypes import windll
六、完整代码
import imaplib
import email
import time
import re
import os
import win32com.client
from datetime import datetime, timedelta
import logging
import configparser
import sys
import poplib
import win32gui
import win32con
import win32api
import win32ui
from ctypes import windll
# 配置日志输出格式和位置
logging.basicConfig(
level=logging.INFO, # 设置日志级别为INFO
format='%(asctime)s - %(levelname)s - %(message)s', # 设置日志格式
handlers=[
logging.FileHandler('thunder_downloader.log', encoding='utf-8'), # 输出到文件
logging.StreamHandler(sys.stdout) # 同时输出到控制台
]
)
class ThunderMailDownloader:
"""迅雷邮件下载器类
实现邮件检查、链接提取、迅雷控制等功能
"""
def __init__(self):
"""初始化下载器
从配置文件读取邮箱凭据和设置必要的参数
"""
# 读取配置文件
config = configparser.ConfigParser()
config.read('config.ini')
self.email_address = config['Email']['email_address']
self.email_password = config['Email']['email_password']
self.pop3_server = "pop.126.com" # POP3服务器地址
self.thunder_path = r"迅雷可执行文件的路径"
self.check_count = 0 # 检查次数计数器
self.last_check_time = None # 上次检查时间
self.processed_emails = set() # 存储已处理的邮件ID
# 尝试从文件加载已处理的邮件记录
try:
if os.path.exists('processed_emails.txt'):
with open('processed_emails.txt', 'r') as f:
self.processed_emails = set(f.read().splitlines())
except Exception as e:
logging.error(f"加载已处理邮件记录失败: {str(e)}")
def save_processed_emails(self):
"""保存已处理的邮件ID到文件"""
try:
with open('processed_emails.txt', 'w') as f:
for email_id in self.processed_emails:
f.write(f"{email_id}\n")
except Exception as e:
logging.error(f"保存已处理邮件记录失败: {str(e)}")
def get_email_id(self, msg_num):
"""获取邮件的唯一标识符
Args:
msg_num: 邮件序号
Returns:
str: 邮件的唯一标识符
"""
try:
# 获取邮件的UIDL(唯一标识符)
uidl = self.mail.uidl(msg_num).decode('utf-8')
# 提取邮件ID
email_id = uidl.split(' ')[1]
return email_id
except Exception as e:
logging.error(f"获取邮件ID失败: {str(e)}")
# 如果获取UIDL失败,使用邮件序号和大小作为标识
try:
size = self.mail.list(msg_num).decode('utf-8').split(' ')[2]
return f"{msg_num}_{size}"
except:
return str(msg_num)
def connect_to_email(self):
"""连接到邮箱服务器
使用POP3_SSL协议连接126邮箱
Returns:
bool: 连接是否成功
"""
try:
print(f"正在连接邮箱 {self.email_address}...")
print("正在连接到POP3服务器...")
self.mail = poplib.POP3_SSL(self.pop3_server)
self.mail.set_debuglevel(1) # 启用调试模式,显示详细连接信息
print("正在尝试登录...")
self.mail.user(self.email_address)
self.mail.pass_(self.email_password)
# 获取邮箱统计信息
stat = self.mail.stat()
print(f"邮箱连接成功!共有 {stat[0]} 封邮件,总大小 {stat[1]} 字节")
return True
except Exception as e:
print("\n邮箱连接失败!可能的原因:")
print("1. 邮箱账号或密码错误")
print("2. 未开启POP3服务")
print("3. 授权码不正确")
print("\n请检查:")
print("1. 确保已在126邮箱设置中开启POP3服务")
print("2. 确保使用的是最新的授权码而不是登录密码")
print("3. 确保邮箱地址格式正确")
print(f"\n具体错误信息: {str(e)}")
logging.error(f"邮箱连接失败: {str(e)}")
return False
def extract_download_links(self, email_content):
"""从邮件内容中提取下载链接
支持迅雷链接和磁力链接
Args:
email_content: 邮件内容文本
Returns:
list: 提取到的下载链接列表
"""
links = []
# 匹配迅雷链接(thunder://开头)
thunder_pattern = r"thunder://[A-Za-z0-9+/=]+"
thunder_links = re.findall(thunder_pattern, email_content)
links.extend(thunder_links)
# 匹配磁力链接(magnet:?xt=urn:btih:开头)
magnet_pattern = r"magnet:\?xt=urn:btih:[A-Fa-f0-9]{40}"
magnet_links = re.findall(magnet_pattern, email_content)
links.extend(magnet_links)
return links
def check_emails(self):
"""检查未读邮件
获取最近的未读邮件,提取其中的下载链接
"""
try:
print("正在检查未读邮件...")
# 获取邮件列表
num_messages = len(self.mail.list()[1])
if num_messages == 0:
print("没有邮件")
return
# 获取未读邮件
unread_messages = []
print("正在查找未读邮件...")
# 从最新的邮件开始检查
for i in range(num_messages, 0, -1):
try:
# 获取邮件的唯一标识符
email_id = self.get_email_id(i)
# 如果邮件已经处理过,跳过
if email_id in self.processed_emails:
continue
# 获取邮件头信息
resp = self.mail.top(i, 0)
msg_lines = resp[1]
msg_content = b'\n'.join(msg_lines).decode('utf-8', errors='ignore')
# 检查邮件是否未读(通过多个标记判断)
is_unread = ('Status: RO' not in msg_content and
'X-Status: R' not in msg_content and
'Status: R' not in msg_content)
if is_unread:
unread_messages.append((i, email_id))
print(f"发现未读邮件: 第 {i} 封")
if len(unread_messages) >= 3: # 限制处理最近3封未读邮件
break
except Exception as e:
print(f"检查邮件状态时出错: {str(e)}")
continue
if not unread_messages:
print("没有未读邮件")
return
print(f"\n发现 {len(unread_messages)} 封未读邮件,开始处理...")
# 处理每封未读邮件
for msg_num, email_id in unread_messages:
try:
print(f"\n正在处理第 {msg_num} 封邮件...")
# 获取完整邮件内容
message_lines = self.mail.retr(msg_num)[1]
message_content = b'\n'.join(message_lines).decode('utf-8', errors='ignore')
email_message = email.message_from_string(message_content)
# 获取邮件主题和发件人
subject = email.header.make_header(email.header.decode_header(email_message['Subject']))
from_addr = email.header.make_header(email.header.decode_header(email_message['From']))
print(f"主题: {subject}")
print(f"发件人: {from_addr}")
# 处理邮件内容(支持多部分邮件)
if email_message.is_multipart():
for part in email_message.walk():
if part.get_content_type() == "text/plain":
content = part.get_payload(decode=True).decode('utf-8', errors='ignore')
download_links = self.extract_download_links(content)
if download_links:
self.start_thunder_download(download_links)
else:
content = email_message.get_payload(decode=True).decode('utf-8', errors='ignore')
download_links = self.extract_download_links(content)
if download_links:
self.start_thunder_download(download_links)
# 将邮件标记为已处理
self.processed_emails.add(email_id)
self.save_processed_emails()
except Exception as e:
print(f"处理邮件时发生错误: {str(e)}")
continue
except Exception as e:
print(f"检查邮件时发生错误: {str(e)}")
logging.error(f"检查邮件时发生错误: {str(e)}")
finally:
try:
self.mail.quit()
except:
pass
def find_thunder_window(self):
"""查找迅雷主窗口
Returns:
int: 窗口句柄,未找到则返回None
"""
def callback(hwnd, windows):
if win32gui.IsWindowVisible(hwnd):
title = win32gui.GetWindowText(hwnd)
class_name = win32gui.GetClassName(hwnd)
print(f"找到窗口: 标题='{title}' 类名='{class_name}'")
if title == "迅雷下载" and class_name == "CabinetWClass":
windows.append(hwnd)
return True
windows = []
win32gui.EnumWindows(callback, windows)
return windows[0] if windows else None
def find_download_dialog(self):
"""查找新建下载任务窗口
Returns:
int: 窗口句柄,未找到则返回None
"""
def callback(hwnd, windows):
if win32gui.IsWindowVisible(hwnd):
title = win32gui.GetWindowText(hwnd)
class_name = win32gui.GetClassName(hwnd)
print(f"检查窗口: 标题='{title}' 类名='{class_name}'")
if any(keyword in title for keyword in ["新建任务", "新建下载任务", "下载任务"]):
print(f"找到下载任务窗口: 标题='{title}' 类名='{class_name}'")
windows.append((hwnd, title, class_name))
return True
windows = []
win32gui.EnumWindows(callback, windows)
if windows:
# 按标题匹配度排序,选择最匹配的窗口
windows.sort(key=lambda x: len([k for k in ["新建任务", "新建下载任务", "下载任务"] if k in x[1]]),
reverse=True)
return windows[0][0]
return None
def click_button(self, hwnd):
"""模拟点击立即下载按钮
Args:
hwnd: 窗口句柄
Returns:
bool: 点击是否成功
"""
try:
print("开始查找下载按钮...")
# 查找Chrome渲染窗口
def find_chrome_window(child_hwnd, chrome_windows):
try:
class_name = win32gui.GetClassName(child_hwnd)
if class_name == "Chrome_RenderWidgetHostHWND":
rect = win32gui.GetWindowRect(child_hwnd)
size = (rect[2] - rect[0], rect[3] - rect[1])
print(
f"找到Chrome渲染窗口: 类名='{class_name}' 尺寸={size} 位置=({rect[0]}, {rect[1]}, {rect[2]}, {rect[3]})")
chrome_windows.append((child_hwnd, rect))
except Exception as e:
print(f"检查Chrome窗口时出错: {str(e)}")
return True
chrome_windows = []
win32gui.EnumChildWindows(hwnd, find_chrome_window, chrome_windows)
if not chrome_windows:
print("未找到Chrome渲染窗口")
return False
# 获取最大的Chrome渲染窗口
chrome_hwnd, chrome_rect = max(chrome_windows, key=lambda x: (x[1][2] - x[1][0]) * (x[1][3] - x[1][1]))
# 计算窗口尺寸
window_width = chrome_rect[2] - chrome_rect[0]
window_height = chrome_rect[3] - chrome_rect[1]
# 定义可能的按钮位置
button_positions = [
# 位置1:底部中间
(chrome_rect[0] + window_width // 2, chrome_rect[3] - 70),
# 位置2:底部中间偏左
(chrome_rect[0] + window_width // 3, chrome_rect[3] - 70),
# 位置3:底部中间偏右
(chrome_rect[0] + (window_width * 2) // 3, chrome_rect[3] - 70),
# 位置4:底部中间稍高
(chrome_rect[0] + window_width // 2, chrome_rect[3] - 80),
# 位置5:底部中间稍低
(chrome_rect[0] + window_width // 2, chrome_rect[3] - 60)
]
# 激活窗口
win32gui.SetForegroundWindow(hwnd)
time.sleep(1)
# 尝试点击每个位置
for x, y in button_positions:
print(f"尝试点击位置: x={x}, y={y}")
win32api.SetCursorPos((x, y))
time.sleep(0.5)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
time.sleep(0.2)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(1)
print("已完成所有点击位置尝试")
return True
except Exception as e:
print(f"点击按钮时发生错误: {str(e)}")
return False
def start_thunder_download(self, links):
"""启动迅雷下载
Args:
links: 下载链接列表
"""
try:
print(f"发现 {len(links)} 个下载链接")
for link in links:
print(f"正在启动迅雷下载链接...")
if link.startswith('magnet:'):
print("检测到磁力链接")
elif link.startswith('thunder:'):
print("检测到迅雷链接")
os.startfile(link)
logging.info(f"已启动迅雷下载链接: {link}")
# 等待迅雷启动和界面加载
print("等待迅雷启动...")
time.sleep(10)
# 等待新建任务窗口出现
print("等待新建任务窗口出现...")
max_attempts = 20
dialog_hwnd = None
for attempt in range(max_attempts):
time.sleep(1)
dialog_hwnd = self.find_download_dialog()
if dialog_hwnd:
print("找到新建任务窗口")
print("等待窗口加载完成...")
time.sleep(6)
if self.click_button(dialog_hwnd):
print("已完成点击尝试")
break
print("点击未成功,继续等待...")
else:
print(f"等待新建任务窗口...(第 {attempt + 1}/{max_attempts} 次)")
# 处理完一个链接后等待
time.sleep(5)
except Exception as e:
print(f"启动迅雷下载失败: {str(e)}")
logging.error(f"启动迅雷下载失败: {str(e)}")
def run(self):
"""运行下载器主循环"""
print("\n=== 迅雷邮件下载程序已启动 ===")
print(f"邮箱账号: {self.email_address}")
print("程序将每小时检查一次邮箱")
print("支持迅雷链接和磁力链接下载")
print("按 Ctrl+C 可以停止程序")
print("=" * 40 + "\n")
while True:
try:
self.check_count += 1
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"\n第 {self.check_count} 次检查 - {current_time}")
if self.connect_to_email():
try:
self.check_emails()
finally:
try:
self.mail.quit()
except:
pass
self.last_check_time = current_time
next_check_time = (datetime.now() + timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S")
print(f"\n下次检查时间: {next_check_time}")
print("程序正在运行中...")
time.sleep(3600) # 等待1小时
except KeyboardInterrupt:
print("\n\n程序已停止运行")
print(f"共检查了 {self.check_count} 次")
if self.last_check_time:
print(f"最后一次检查时间: {self.last_check_time}")
break
except Exception as e:
print(f"发生错误: {str(e)}")
logging.error(f"程序运行错误: {str(e)}")
time.sleep(60) # 发生错误时等待1分钟后重试
if __name__ == "__main__":
try:
downloader = ThunderMailDownloader()
downloader.run()
except Exception as e:
print(f"程序发生错误: {str(e)}")
logging.error(f"程序发生错误: {str(e)}")
input("按回车键退出...")
七、常见问题
1. 邮箱连接失败
- 检查网络连接 - 验证邮箱配置 - 确认POP3服务是否开启
2. 窗口识别问题
- 确认迅雷版本兼容性 - 检查窗口标题是否匹配 - 查看日志中的窗口信息
3. 点击未生效
- 检查窗口焦点 - 确认按钮位置计算 - 调整等待时间