智能邮件群发系统
一个基于Python和PyQt5开发的智能邮件群发工具,支持Word模板和Excel数据源的自动匹配,具有现代化UI界面和友好的用户体验。
Github项目地址:https://github.com/liugang926/Auto-mail-sent.git
dist目录有编译好的exe程序,可直接使用。
功能特点
- 支持Word文档作为邮件模板
- 支持Excel表格作为收件人数据源
- 智能识别并自动匹配变量
- 自动识别姓名和邮箱列
- 实时邮件预览功能
- 未匹配变量智能提示
- 可配置发送时间间隔
- 发送进度实时显示
- 支持中断发送任务
- 邮箱配置测试功能
- 现代化UI界面设计
系统要求
- Python 3.7+
- Windows/Linux/MacOS
- Microsoft Visual C++ 14.0 或更高版本
快速开始
- 克隆项目
git clone [https://github.com/liugang926/Auto-mail-sent.git]
cd email-sender
- 创建虚拟环境(推荐)
python -m venv venv
# Windows
venv\Scripts\activate
# Linux/Mac
source venv/bin/activate
- 安装依赖
pip install -r requirements.txt
- 运行程序
python main.py
项目结构
email_sender/
│
├── main.py # 主程序入口
├── ui.py # UI界面实现
├── email_processor.py # 邮件处理模块
├── word_reader.py # Word文档读取
├── excel_reader.py # Excel文件读取
├── config.ini # 配置文件
└── requirements.txt # 依赖包列表
主要模块功能
main.py
: 程序入口,初始化应用ui.py
: 实现图形界面和用户交互email_processor.py
: 处理邮件发送逻辑word_reader.py
: 处理Word模板读取excel_reader.py
: 处理Excel数据读取
打包说明
环境准备
- 安装PyInstaller
pip install pyinstaller
- 确保所需资源文件存在:
- config.ini(邮箱配置文件)
- email.png(程序图标)
- README.md(说明文档)
打包步骤
- 运行打包脚本
python setup.py
- 打包过程说明:
- 清理旧的构建文件
- 创建版本信息
- 构建可执行文件
- 复制必要资源
- 清理临时文件
- 打包完成后,在dist目录下可以找到:
- 邮件群发工具.exe(主程序)
- config.ini(配置文件)
- README.md(说明文档)
- email.png(程序图标)
打包注意事项
- 确保所有依赖包已正确安装
- 确保资源文件完整
- 需要管理员权限运行打包脚本
- 打包过程可能需要几分钟时间
配置说明
邮箱配置 (config.ini)
[EMAIL]
sender_name = 发件人姓名
sender_email = your_email@example.com
smtp_server = smtp.example.com
smtp_port = 587
smtp_password = your_password
use_ssl = True
常见邮箱服务器设置
Gmail
smtp_server = smtp.gmail.com
smtp_port = 587
use_ssl = True
注意:需要开启两步验证并使用应用专用密码
QQ邮箱
smtp_server = smtp.qq.com
smtp_port = 465
use_ssl = True
注意:密码需要使用授权码
163邮箱
smtp_server = smtp.163.com
smtp_port = 465
use_ssl = True
Word模板变量
模板中支持以下变量:
{name}
: 收件人姓名{email}
: 收件人邮箱
使用说明
1. 文件准备
Word模板要求
- 使用 {变量名} 格式插入变量
- 变量名需要与Excel表格的列名完全一致
- 支持任意数量的变量
示例:
使用指南
-
准备工作
- 创建Word邮件模板
- 准备Excel收件人数据
- 配置config.ini文件
-
启动程序
python main.py
-
操作步骤
- 选择Word模板文件
- 选择Excel数据文件
- 选择姓名和邮箱列
- 填写邮件主题
- 设置发送间隔
- 测试邮箱配置
- 生成预览确认
- 开始发送
注意事项
-
发送前检查事项:
- 确保网络连接正常
- 验证邮箱配置正确
- 检查模板格式无误
- 确认收件人数据完整
-
发送建议:
- 首次使用建议先测试配置
- 大量发送时适当增加间隔
- 定期检查发送状态
- 注意邮件服务商限制
常见问题解决
1. 打包相关
-
Q: 打包失败,提示缺少依赖
- A: 检查requirements.txt中的包是否都已安装
- A: 尝试重新安装PyInstaller
-
Q: 运行exe文件报错
- A: 确保所有资源文件在正确位置
- A: 检查是否缺少Visual C++运行库
2. 发送相关
-
Q: 无法连接SMTP服务器
- A: 检查网络连接
- A: 验证服务器地址和端口
- A: 确认SSL设置是否正确
-
Q: 认证失败
- A: 检查账号密码
- A: 确认是否需要使用授权码
- A: 验证邮箱服务是否开启SMTP
技术支持
如遇问题,请按以下步骤处理:
- 检查配置文件设置
- 查看程序运行日志
- 确认网络连接状态
- 提交Issue或联系技术支持
版本历史
- v1.0.0
- 基础邮件发送功能
- Word模板和Excel数据支持
- 现代化UI界面
- 邮箱配置测试
- 打包功能支持
- 邮件和姓名以及内容的其他变量自动匹配
许可说明
本项目仅供学习和参考使用。在使用本工具时,请遵守:
- 相关法律法规
- 邮件服务商的使用规范
- 用户隐私保护规定
### config.ini```bash
[EMAIL]
sender_name = 发件人姓名
sender_email = your_email@example.com
smtp_server = smtp.example.com
smtp_port = 587
smtp_password = your_password
use_ssl = True
main.py
import sys
from PyQt5.QtWidgets import QApplication
from ui import MainWindow
import resources_rc # 导入编译后的资源文件if __name__ == "__main__":app = QApplication(sys.argv)window = MainWindow()window.show()sys.exit(app.exec_())
ui.py
import os
import sys
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QFileDialog, QSpinBox, QTextEdit, QProgressBar, QComboBox,QGroupBox, QFormLayout, QMessageBox, QDialog,QListWidget)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QFont, QPixmap, QIcon
from qt_material import apply_stylesheet
from email_processor import EmailSender
from word_reader import WordReader
from excel_reader import ExcelReader
import pandas as pd
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from datetime import datetime
from PyQt5.QtWidgets import QApplicationclass BlurredWidget(QWidget):"""实现毛玻璃效果的基础Widget"""def __init__(self, parent=None):super().__init__(parent)self.setAttribute(Qt.WA_TranslucentBackground)self.setStyleSheet("""QWidget {background-color: rgba(255, 255, 255, 180);border-radius: 10px;}""")class EmailPreviewWidget(QWidget):"""邮件预览窗口"""def __init__(self, parent=None):super().__init__(parent)layout = QVBoxLayout(self)self.subject_label = QLabel("主题: ")self.to_label = QLabel("收件人: ")self.content = QTextEdit()self.content.setReadOnly(True)layout.addWidget(self.subject_label)layout.addWidget(self.to_label)layout.addWidget(self.content)def update_preview(self, subject, to_name, to_email, content):self.subject_label.setText(f"主题: {subject}")self.to_label.setText(f"收件人: {to_name} <{to_email}>")self.content.setHtml(content)class EmailSenderThread(QThread):"""邮件发送线程"""progress_updated = pyqtSignal(int)email_sent = pyqtSignal(str, str)finished = pyqtSignal()error = pyqtSignal(str)def __init__(self, email_sender, data, template, variable_columns, subject, interval):super().__init__()self.email_sender = email_senderself.data = dataself.template = templateself.variable_columns = variable_columnsself.subject = subjectself.interval = intervalself.is_running = Truedef run(self):total = len(self.data)for i, row in enumerate(self.data):if not self.is_running:breaktry:# 替换所有变量content = self.templatefor col in self.variable_columns:content = content.replace(f"{{{col}}}", str(row[col]))self.email_sender.send_email(row['email'], self.subject, content)self.email_sent.emit(row['name'], row['email'])# 更新进度progress = int((i + 1) / total * 100)self.progress_updated.emit(progress)# 按指定间隔暂停self.msleep(self.interval * 1000)except Exception as e:self.error.emit(f"发送给 {row['name']} <{row['email']}> 失败: {str(e)}")self.finished.emit()def stop(self):self.is_running = Falseclass MainWindow(QMainWindow):def __init__(self):super().__init__()self.setWindowTitle("智能邮件群发系统")self.setMinimumSize(900, 700)# 设置窗口图标icon = QIcon(":/icons/email.png") # 使用Qt资源系统self.setWindowIcon(icon)# 设置默认字体app = QApplication.instance()font = QFont("Microsoft YaHei UI", 9) # 使用微软雅黑UI字体app.setFont(font)# 设置窗口背景self.setObjectName("mainWindow")# 初始化读取器和发送器self.word_reader = WordReader()self.excel_reader = ExcelReader()self.email_sender = EmailSender()# 数据存储self.template_content = ""self.excel_data = Noneself.name_column = ""self.email_column = ""# 先创建UIself.setup_ui()# 设置特殊按钮的ObjectName (移到UI创建之后)self.test_send_btn.setObjectName("test_send_btn")self.stop_btn.setObjectName("stop_btn")# 应用样式self.apply_blur_style()def setup_ui(self):# 主容器central_widget = QWidget()main_layout = QVBoxLayout(central_widget)main_layout.setContentsMargins(20, 20, 20, 20)main_layout.setSpacing(15)# ===== 文件选择区域 =====file_group = QGroupBox()file_group.setTitle("文件选择")file_layout = QFormLayout()file_layout.setSpacing(12)file_layout.setContentsMargins(15, 25, 15, 15)# Word模板选择word_layout = QHBoxLayout()self.word_path = QLineEdit()self.word_path.setReadOnly(True)self.word_path.setMinimumHeight(32) # 增加高度word_browse_btn = QPushButton("浏览...")word_browse_btn.setFixedSize(90, 32) # 固定按钮大小word_browse_btn.clicked.connect(self.browse_word)word_layout.addWidget(self.word_path)word_layout.addWidget(word_browse_btn)word_layout.setSpacing(10)# Excel数据选择excel_layout = QHBoxLayout()self.excel_path = QLineEdit()self.excel_path.setReadOnly(True)self.excel_path.setMinimumHeight(32) # 增加高度excel_browse_btn = QPushButton("浏览...")excel_browse_btn.setFixedSize(90, 32) # 固定按钮大小excel_browse_btn.clicked.connect(self.browse_excel)excel_layout.addWidget(self.excel_path)excel_layout.addWidget(excel_browse_btn)excel_layout.setSpacing(10)file_layout.addRow("Word模板:", word_layout)file_layout.addRow("Excel数据:", excel_layout)file_group.setLayout(file_layout)# ===== 邮件配置区域 =====config_group = QGroupBox("邮件配置")config_layout = QFormLayout()config_layout.setSpacing(12)config_layout.setContentsMargins(15, 25, 15, 15)# 变量匹配状态显示self.variables_status = QLabel("变量匹配状态")self.variables_status.setWordWrap(True)# 主题输入框self.subject_input = QLineEdit()self.subject_input.setMinimumHeight(32)# 发送间隔设置self.interval_spinbox = QSpinBox()self.interval_spinbox.setMinimumHeight(32)self.interval_spinbox.setRange(1, 600)self.interval_spinbox.setValue(30)self.interval_spinbox.setSuffix(" 秒")# 将组件添加到配置布局config_layout.addRow("变量状态:", self.variables_status)config_layout.addRow("邮件主题:", self.subject_input)config_layout.addRow("发送间隔:", self.interval_spinbox)# 测试按钮和帮助按钮test_btn_layout = QHBoxLayout()self.test_send_btn = QPushButton("测试邮箱配置")self.test_send_btn.setFixedSize(120, 36)self.test_send_btn.clicked.connect(self.test_email_config)# 添加帮助按钮help_btn = QPushButton("帮助")help_btn.setObjectName("help_btn")help_btn.setFixedSize(80, 36)help_btn.clicked.connect(self.show_help)test_btn_layout.addWidget(self.test_send_btn)test_btn_layout.addWidget(help_btn)test_btn_layout.addStretch()config_layout.addRow("", test_btn_layout)config_group.setLayout(config_layout)# ===== 预览和进度区域 =====bottom_layout = QHBoxLayout()bottom_layout.setSpacing(15)# 预览区域preview_group = QGroupBox()preview_group.setTitle("邮件预览")preview_layout = QVBoxLayout()preview_layout.setSpacing(12)preview_layout.setContentsMargins(15, 25, 15, 15)self.preview_widget = EmailPreviewWidget()preview_layout.addWidget(self.preview_widget)# 进度区域progress_group = QGroupBox()progress_group.setTitle("发送进度")progress_layout = QVBoxLayout()progress_layout.setSpacing(12)progress_layout.setContentsMargins(15, 25, 15, 15)self.progress_bar = QProgressBar()self.progress_bar.setMinimumHeight(24)self.status_label = QLabel("就绪")self.status_label.setMinimumHeight(36)btn_layout = QHBoxLayout()btn_layout.setSpacing(10)self.send_btn = QPushButton("开始发送")self.send_btn.setFixedHeight(36)self.send_btn.clicked.connect(self.start_sending)self.stop_btn = QPushButton("停止发送")self.stop_btn.setFixedHeight(36)self.stop_btn.clicked.connect(self.stop_sending)self.stop_btn.setEnabled(False)btn_layout.addWidget(self.send_btn)btn_layout.addWidget(self.stop_btn)progress_layout.addWidget(self.progress_bar)progress_layout.addWidget(self.status_label)progress_layout.addLayout(btn_layout)progress_layout.addStretch()progress_group.setLayout(progress_layout)# 设置预览和进度区域的比例bottom_layout.addWidget(preview_group, 2)bottom_layout.addWidget(progress_group, 1)# 添加所有组件到主布局main_layout.addWidget(file_group)main_layout.addWidget(config_group)main_layout.addLayout(bottom_layout, 1)self.setCentralWidget(central_widget)def apply_blur_style(self):"""应用现代化UI风格"""self.setStyleSheet("""* {font-family: "Microsoft YaHei UI", "Microsoft YaHei", "SimHei", sans-serif;}QMainWindow {background-color: #f8f9fa;}QGroupBox {background-color: white;border-radius: 8px;border: 1px solid #e9ecef;margin-top: 20px;padding: 28px 15px 15px 15px;font-weight: 500;font-size: 14px;color: #2c3e50;}QGroupBox::title {subcontrol-origin: margin;subcontrol-position: top left;left: 15px;top: 10px;padding: 0px 10px;background-color: white;color: #2c3e50;font-size: 14px;font-weight: 500;}QPushButton {background-color: #3498db;color: white;border-radius: 4px;padding: 8px 16px;border: none;font-weight: 500;font-size: 13px;min-width: 80px;min-height: 32px;}QPushButton:hover {background-color: #2980b9;}QPushButton:pressed {background-color: #2473a7;}QPushButton:disabled {background-color: #bdc3c7;}QLineEdit, QTextEdit, QComboBox, QSpinBox {background-color: white;border-radius: 4px;border: 1px solid #ced4da;padding: 6px 12px;color: #2c3e50;font-size: 13px;min-height: 32px;}QLineEdit:focus, QTextEdit:focus, QComboBox:focus, QSpinBox:focus {border: 2px solid #3498db;background-color: white;}QLabel {color: #2c3e50;font-size: 13px;padding: 4px 0;font-weight: normal;}QProgressBar {border: none;border-radius: 4px;text-align: center;background-color: #e9ecef;font-size: 12px;color: white;min-height: 24px;}QProgressBar::chunk {background-color: #2ecc71;border-radius: 4px;}/* 特殊按钮样式 */QPushButton#test_send_btn {background-color: #2ecc71;}QPushButton#test_send_btn:hover {background-color: #27ae60;}QPushButton#stop_btn {background-color: #e74c3c;}QPushButton#stop_btn:hover {background-color: #c0392b;}/* 下拉框样式 */QComboBox::drop-down {border: none;width: 30px;}QComboBox::down-arrow {image: none;border-left: 5px solid transparent;border-right: 5px solid transparent;border-top: 5px solid #495057;margin-right: 8px;}/* 帮助按钮样式 */QPushButton#help_btn {background-color: #6c757d;color: white;border-radius: 4px;padding: 8px 16px;border: none;font-weight: bold;font-size: 13px;}QPushButton#help_btn:hover {background-color: #5a6268;}QPushButton#help_btn:pressed {background-color: #545b62;}""")def browse_word(self):file_path, _ = QFileDialog.getOpenFileName(self, "选择Word模板", "", "Word文档 (*.docx *.doc)")if file_path:self.word_path.setText(file_path)try:self.template_content, self.template_variables = self.word_reader.read_template(file_path)# 显示找到的变量variables_text = "模板中的变量:\n" + "\n".join([f"{{{var}}}" for var in self.template_variables])self.variables_status.setText(variables_text)# 如果已经加载了Excel,检查变量匹配if self.excel_data:self.check_variable_matching()QMessageBox.information(self, "成功", f"Word模板加载成功!\n找到 {len(self.template_variables)} 个变量。")except Exception as e:QMessageBox.critical(self, "错误", f"无法读取Word文档: {str(e)}")def browse_excel(self):"""修改Excel文件选择处理"""file_path, _ = QFileDialog.getOpenFileName(self, "选择Excel数据文件", "", "Excel文件 (*.xlsx *.xls)")if file_path:self.excel_path.setText(file_path)try:self.excel_data, self.excel_columns = self.excel_reader.read_data(file_path)# 如果已经加载了Word模板,检查变量匹配if hasattr(self, 'template_variables'):self.check_variable_matching()QMessageBox.information(self, "成功", f"Excel数据加载成功!共{len(self.excel_data)}条记录。")except Exception as e:QMessageBox.critical(self, "错误", f"无法读取Excel文件: {str(e)}")def check_variable_matching(self):"""检查Word模板变量与Excel列的匹配情况,并自动生成预览"""if not hasattr(self, 'template_variables') or not hasattr(self, 'excel_columns'):return# 检查变量匹配matched_vars = []unmatched_vars = []self.name_column = Noneself.email_column = None# 自动识别姓名和邮箱列for col in self.excel_columns:if not self.name_column and ("姓名" in col or "名字" in col or "name" in col.lower()):self.name_column = colif not self.email_column and ("邮箱" in col or "邮件" in col or "email" in col.lower()):self.email_column = col# 检查其他变量匹配for var in self.template_variables:if var in self.excel_columns:matched_vars.append(var)else:unmatched_vars.append(var)# 更新变量状态显示status_text = "变量匹配状态:\n\n"# 显示姓名和邮箱列匹配状态if self.name_column:status_text += f"✅ 姓名列: {self.name_column}\n"else:status_text += "❌ 未找到姓名列\n"if self.email_column:status_text += f"✅ 邮箱列: {self.email_column}\n"else:status_text += "❌ 未找到邮箱列\n"status_text += "\n其他变量匹配:\n"if matched_vars:status_text += "✅ 已匹配变量:\n" + "\n".join([f"{{{var}}}" for var in matched_vars]) + "\n\n"if unmatched_vars:status_text += "❌ 未匹配变量:\n" + "\n".join([f"{{{var}}}" for var in unmatched_vars])self.variables_status.setText(status_text)# 自动生成预览self.auto_generate_preview(unmatched_vars)# 显示警告信息warnings = []if not self.name_column:warnings.append("未找到姓名列")if not self.email_column:warnings.append("未找到邮箱列")if unmatched_vars:warnings.append(f"以下变量未找到对应列:{', '.join(unmatched_vars)}")if warnings:QMessageBox.warning(self, "警告", "\n".join(warnings))def auto_generate_preview(self, unmatched_vars=None):"""自动生成预览"""if not self.template_content or not self.excel_data:returnif not self.name_column or not self.email_column:return# 获取第一条数据作为预览try:first_row = self.excel_data[0]content = self.template_content# 替换所有匹配的变量for var in self.template_variables:if var in first_row:content = content.replace(f"{{{var}}}", str(first_row[var]))elif var in unmatched_vars:# 对于未匹配的变量,保留原样显示content = content.replace(f"{{{var}}}", f"[未匹配变量: {{{var}}}]")# 获取主题(如果未输入,使用默认值)subject = self.subject_input.text() or "[请输入邮件主题]"# 更新预览self.preview_widget.update_preview(subject,first_row[self.name_column],first_row[self.email_column],content)except Exception as e:self.preview_widget.update_preview("[请输入邮件主题]","预览生成失败","预览生成失败",f"生成预览时发生错误: {str(e)}")def start_sending(self):if not self.template_content:QMessageBox.warning(self, "警告", "请先加载Word模板!")returnif not self.excel_data:QMessageBox.warning(self, "警告", "请先加载Excel数据!")returnself.name_column = self.name_columnself.email_column = self.email_columnif not self.name_column or not self.email_column:QMessageBox.warning(self, "警告", "请选择姓名和邮箱列!")returnsubject = self.subject_input.text()if not subject:QMessageBox.warning(self, "警告", "请输入邮件主题!")return# 获取选中的变量列selected_items = self.columns_list.selectedItems()selected_columns = [item.text() for item in selected_items]# 准备数据data = []for row in self.excel_data:try:item = {"name": row[self.name_column],"email": row[self.email_column],}# 添加选中的变量数据for col in selected_columns:item[col] = row[col]data.append(item)except Exception as e:print(f"跳过无效数据: {row}, 错误: {str(e)}")# 创建并启动发送线程self.sender_thread = EmailSenderThread(self.email_sender,data,self.template_content,selected_columns, # 传递选中的列名subject,self.interval_spinbox.value())self.sender_thread.progress_updated.connect(self.update_progress)self.sender_thread.email_sent.connect(self.on_email_sent)self.sender_thread.finished.connect(self.on_sending_finished)self.sender_thread.error.connect(self.on_sending_error)self.sender_thread.start()# 更新UI状态self.send_btn.setEnabled(False)self.stop_btn.setEnabled(True)self.status_label.setText("发送中...")self.progress_bar.setValue(0)def stop_sending(self):if hasattr(self, "sender_thread") and self.sender_thread.isRunning():self.sender_thread.stop()self.status_label.setText("正在停止...")self.stop_btn.setEnabled(False)def update_progress(self, value):self.progress_bar.setValue(value)def on_email_sent(self, name, email):self.status_label.setText(f"已发送至: {name} <{email}>")def on_sending_finished(self):self.send_btn.setEnabled(True)self.stop_btn.setEnabled(False)self.status_label.setText("发送完成!")QMessageBox.information(self, "成功", "所有邮件已发送完成!")def on_sending_error(self, error_msg):self.status_label.setText(f"错误: {error_msg}")def test_email_config(self):"""测试邮箱配置是否正确"""try:# 创建测试对话框dialog = EmailTestDialog(self)dialog.exec_()except Exception as e:QMessageBox.critical(self, "错误", f"测试发送失败: {str(e)}")def show_help(self):"""显示帮助对话框"""dialog = HelpDialog(self)dialog.exec_()def update_selected_variables(self):"""更新选中的变量列表"""selected_items = self.columns_list.selectedItems()selected_vars = [item.text() for item in selected_items]# 构建变量提示文本vars_text = "已选变量:\n"if selected_vars:vars_text += "\n".join([f"{{{var}}}" for var in selected_vars])else:vars_text += "(无)"self.selected_vars_label.setText(vars_text)# 更新预览self.auto_generate_preview()class EmailTestDialog(QDialog):def __init__(self, parent=None):super().__init__(parent)self.email_sender = EmailSender()self.setup_ui()def setup_ui(self):self.setWindowTitle("邮箱配置测试")self.setMinimumWidth(400)layout = QVBoxLayout(self)# 显示当前配置信息config_group = QGroupBox("当前配置")config_layout = QFormLayout()sender_name = self.email_sender.sender_namesender_email = self.email_sender.sender_emailsmtp_server = self.email_sender.smtp_serversmtp_port = str(self.email_sender.smtp_port)use_ssl = "是" if self.email_sender.use_ssl else "否"config_layout.addRow("发件人:", QLabel(f"{sender_name} <{sender_email}>"))config_layout.addRow("SMTP服务器:", QLabel(smtp_server))config_layout.addRow("SMTP端口:", QLabel(smtp_port))config_layout.addRow("使用SSL:", QLabel(use_ssl))config_group.setLayout(config_layout)layout.addWidget(config_group)# 测试进度和结果self.status_label = QLabel("准备测试...")layout.addWidget(self.status_label)self.progress = QProgressBar()self.progress.setRange(0, 3)self.progress.setValue(0)layout.addWidget(self.progress)# 按钮btn_layout = QHBoxLayout()self.test_btn = QPushButton("开始测试")self.test_btn.clicked.connect(self.run_test)self.close_btn = QPushButton("关闭")self.close_btn.clicked.connect(self.close)btn_layout.addWidget(self.test_btn)btn_layout.addWidget(self.close_btn)layout.addLayout(btn_layout)# 开始测试QTimer.singleShot(100, self.run_test)def run_test(self):self.test_btn.setEnabled(False)self.progress.setValue(0)try:# 测试SMTP连接self.status_label.setText("正在连接SMTP服务器...")self.progress.setValue(1)if self.email_sender.use_ssl:server = smtplib.SMTP_SSL(self.email_sender.smtp_server, self.email_sender.smtp_port)else:server = smtplib.SMTP(self.email_sender.smtp_server, self.email_sender.smtp_port)server.starttls()# 测试登录self.status_label.setText("正在验证登录信息...")self.progress.setValue(2)server.login(self.email_sender.sender_email, self.email_sender.smtp_password)# 发送测试邮件self.status_label.setText("正在发送测试邮件...")self.progress.setValue(3)# 创建测试邮件msg = MIMEMultipart('alternative')msg['From'] = f"{self.email_sender.sender_name} <{self.email_sender.sender_email}>"msg['To'] = self.email_sender.sender_emailmsg['Subject'] = Header("邮箱配置测试", 'utf-8')html_content = f"""<html><body><h3>邮箱配置测试成功</h3><p>这是一封测试邮件,用于验证邮箱配置是否正确。</p><p>配置信息:</p><ul><li>发件人:{self.email_sender.sender_name} <{self.email_sender.sender_email}></li><li>SMTP服务器:{self.email_sender.smtp_server}</li><li>SMTP端口:{self.email_sender.smtp_port}</li><li>SSL加密:{'是' if self.email_sender.use_ssl else '否'}</li></ul><p>发送时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p></body></html>"""html_part = MIMEText(html_content, 'html', 'utf-8')msg.attach(html_part)# 发送邮件server.send_message(msg)server.quit()# 测试完成self.status_label.setText("测试完成!配置正确,邮件已发送。")self.progress.setValue(3)QMessageBox.information(self,"测试成功",f"邮箱配置测试成功!\n已向 {self.email_sender.sender_email} 发送测试邮件。")except Exception as e:error_msg = str(e)self.status_label.setText(f"测试失败: {error_msg}")QMessageBox.critical(self,"测试失败",f"邮箱配置测试失败!\n\n错误信息:{error_msg}\n\n""请检查以下内容:\n""1. SMTP服务器地址和端口是否正确\n""2. 邮箱账号和密码是否正确\n""3. 是否已开启SMTP服务\n""4. 如果使用Gmail,是否已开启两步验证并使用应用专用密码")finally:self.test_btn.setEnabled(True) class HelpDialog(QDialog):def __init__(self, parent=None):super().__init__(parent)self.setWindowTitle("使用帮助")self.setMinimumSize(600, 400)layout = QVBoxLayout(self)# 创建文本浏览器self.help_text = QTextEdit()self.help_text.setReadOnly(True)layout.addWidget(self.help_text)# 关闭按钮close_btn = QPushButton("关闭")close_btn.clicked.connect(self.close)close_btn.setFixedWidth(100)btn_layout = QHBoxLayout()btn_layout.addStretch()btn_layout.addWidget(close_btn)layout.addLayout(btn_layout)# 加载帮助文档self.load_help_content()def load_help_content(self):help_content = """
# 智能邮件群发系统使用说明## 1. 基本使用流程### 1.1 选择Word模板
- 点击"浏览..."选择Word文档作为邮件模板
- 在Word模板中使用 {变量名} 格式插入变量
- 变量名需要与Excel表格的列名完全一致
- 系统会自动识别模板中的所有变量### 1.2 选择Excel数据
- 点击"浏览..."选择Excel文件
- 系统会自动识别姓名列和邮箱列
- 自动匹配Word模板中的其他变量
- 自动显示第一条数据的预览效果### 1.3 发送邮件
1. 填写邮件主题
2. 设置发送间隔时间(秒)
3. 确认预览效果无误后点击"开始发送"
4. 可通过进度条查看发送进度
5. 如需停止发送,点击"停止发送"## 2. 变量使用说明### 2.1 变量格式
- 在Word中使用 {变量名} 格式
- 例如:{姓名}、{部门}、{职位}
- 变量名必须与Excel列名完全一致
- 大小写敏感,请注意保持一致### 2.2 自动匹配规则
- 姓名列:自动匹配包含"姓名"、"名字"、"name"的列
- 邮箱列:自动匹配包含"邮箱"、"邮件"、"email"的列
- 其他变量:自动与Excel列名进行匹配
- 未匹配变量会在预览中显示 [未匹配变量: {变量名}]## 3. 注意事项### 3.1 文件准备
- Word模板需为.doc或.docx格式
- Excel文件需为.xls或.xlsx格式
- Excel表格第一行必须为列名
- 确保数据列名与模板变量名一致### 3.2 发送建议
- 首次使用建议先测试邮箱配置
- 发送前请仔细检查预览效果
- 建议适当设置发送间隔时间
- 大量发送时注意邮箱服务限制## 4. 常见问题### 4.1 变量未匹配
- 检查变量名与Excel列名是否完全一致
- 注意大小写、空格等是否一致
- 确认Excel文件第一行是否为列名### 4.2 邮件发送失败
- 检查邮箱配置是否正确
- 确认网络连接是否正常
- 查看是否触发发送频率限制
- 验证收件人邮箱地址是否有效### 4.3 预览显示异常
- 确认Word模板格式是否正确
- 检查Excel数据是否完整
- 验证变量格式是否规范## 5. 技术支持如遇到问题,请检查:
1. 文件格式是否正确
2. 变量名是否匹配
3. 邮箱配置是否有效
4. 网络连接是否正常如需帮助,请联系技术支持。
"""self.help_text.setMarkdown(help_content)
email_processor.py
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
import configparser
import osclass EmailSender:"""邮件发送处理类"""def __init__(self, config_file="config.ini"):self.config = self._load_config(config_file)self.sender_name = self.config.get('EMAIL', 'sender_name')self.sender_email = self.config.get('EMAIL', 'sender_email')self.smtp_server = self.config.get('EMAIL', 'smtp_server')self.smtp_port = self.config.getint('EMAIL', 'smtp_port')self.smtp_password = self.config.get('EMAIL', 'smtp_password')self.use_ssl = self.config.getboolean('EMAIL', 'use_ssl')def _load_config(self, config_file):"""加载配置文件"""if not os.path.exists(config_file):raise FileNotFoundError(f"找不到配置文件: {config_file}")config = configparser.ConfigParser()config.read(config_file, encoding='utf-8')# 验证必要配置required_options = [('EMAIL', 'sender_name'),('EMAIL', 'sender_email'),('EMAIL', 'smtp_server'),('EMAIL', 'smtp_port'),('EMAIL', 'smtp_password')]for section, option in required_options:if not config.has_option(section, option):raise ValueError(f"配置文件中缺少必要的选项: [{section}] {option}")return configdef send_email(self, to_email, subject, html_content):"""发送邮件"""# 创建邮件msg = MIMEMultipart('alternative')msg['From'] = f"{self.sender_name} <{self.sender_email}>"msg['To'] = to_emailmsg['Subject'] = Header(subject, 'utf-8')# 添加HTML内容html_part = MIMEText(html_content, 'html', 'utf-8')msg.attach(html_part)# 连接到SMTP服务器并发送try:if self.use_ssl:server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port)else:server = smtplib.SMTP(self.smtp_server, self.smtp_port)server.starttls()server.login(self.sender_email, self.smtp_password)server.send_message(msg)server.quit()except Exception as e:raise Exception(f"发送邮件失败: {str(e)}")
word_reader.py
import docx
from docx.opc.exceptions import PackageNotFoundError
import os
import html
import re
from docx import Documentclass WordReader:"""Word文档模板读取器"""def read_template(self, file_path):"""读取Word模板并返回内容和变量列表"""if not os.path.exists(file_path):raise FileNotFoundError(f"找不到文件: {file_path}")try:doc = Document(file_path)except PackageNotFoundError:raise ValueError(f"无法打开文件,可能不是有效的Word文档: {file_path}")content = []variables = set() # 使用集合存储找到的所有变量# 遍历所有段落for para in doc.paragraphs:content.append(para.text)# 查找所有 {变量名} 格式的变量vars = re.findall(r'\{([^}]+)\}', para.text)variables.update(vars)return '\n'.join(content), list(variables)def read_template_html(self, file_path):"""读取Word文档内容,并转为HTML格式支持{name}和{email}作为替换变量"""if not os.path.exists(file_path):raise FileNotFoundError(f"找不到文件: {file_path}")try:doc = docx.Document(file_path)except PackageNotFoundError:raise ValueError(f"无法打开文件,可能不是有效的Word文档: {file_path}")# 转换为HTMLhtml_content = []for para in doc.paragraphs:if para.text.strip():# 处理段落样式style = ""if para.style.name.startswith('Heading'):level = para.style.name[-1]html_content.append(f"<h{level}>{html.escape(para.text)}</h{level}>")else:# 处理段落中的格式formatted_text = []for run in para.runs:text = html.escape(run.text)if run.bold:text = f"<strong>{text}</strong>"if run.italic:text = f"<em>{text}</em>"if run.underline:text = f"<u>{text}</u>"formatted_text.append(text)html_content.append(f"<p>{''.join(formatted_text)}</p>")return "\n".join(html_content)
excel_reader.py
import pandas as pd
import osclass ExcelReader:"""Excel文件读取器"""def read_data(self, file_path):"""读取Excel文件数据返回数据列表和列名列表"""if not os.path.exists(file_path):raise FileNotFoundError(f"找不到文件: {file_path}")try:# 读取Exceldf = pd.read_excel(file_path)# 验证数据帧不为空if df.empty:raise ValueError("Excel文件中没有数据")# 将数据转换为列表字典data = df.to_dict(orient='records')# 获取列名columns = df.columns.tolist()return data, columnsexcept Exception as e:raise ValueError(f"读取Excel文件时出错: {str(e)}")
requirements.txt
python-docx==0.8.11
openpyxl==3.1.2
pandas==2.0.3
PyQt5==5.15.9
PyQt5-Qt5==5.15.2
PyQt5-sip==12.12.1
pywin32==306
jinja2==3.1.2
pyinstaller==5.13.2
pillow==10.0.0
qt-material==2.14
python-dotenv==1.0.0
qt-material
setup.py
import PyInstaller.__main__
import os
import shutil
import sys
from datetime import datetime
import site
import PyQt5def get_pyqt_path():"""获取PyQt5安装路径"""return os.path.dirname(PyQt5.__file__)def clean_dist():"""清理dist目录"""if os.path.exists('dist'):shutil.rmtree('dist')os.makedirs('dist')def clean_build():"""清理build目录"""if os.path.exists('build'):shutil.rmtree('build')if os.path.exists('*.spec'):try:os.remove('*.spec')except:passdef copy_resources():"""复制必要的资源文件"""resource_files = ['config.ini','README.md','requirements.txt','email.png' # 程序图标]for file in resource_files:if os.path.exists(file):shutil.copy(file, 'dist/')else:print(f"警告: {file} 文件不存在")def create_version_info():"""创建版本信息文件"""version_info = f"""
VSVersionInfo(ffi=FixedFileInfo(filevers=(1, 0, 0, 0),prodvers=(1, 0, 0, 0),mask=0x3f,flags=0x0,OS=0x40004,fileType=0x1,subtype=0x0,date=(0, 0)),kids=[StringFileInfo([StringTable(u'080404b0',[StringStruct(u'CompanyName', u'Your Company'),StringStruct(u'FileDescription', u'智能邮件群发系统'),StringStruct(u'FileVersion', u'1.0.0'),StringStruct(u'InternalName', u'email_sender'),StringStruct(u'LegalCopyright', u'Copyright (C) {datetime.now().year}'),StringStruct(u'OriginalFilename', u'邮件群发工具.exe'),StringStruct(u'ProductName', u'智能邮件群发系统'),StringStruct(u'ProductVersion', u'1.0.0')])]),VarFileInfo([VarStruct(u'Translation', [2052, 1200])])]
)
"""with open('version_info.txt', 'w', encoding='utf-8') as f:f.write(version_info)def build_executable():"""构建可执行文件"""pyqt_path = get_pyqt_path()# 构建命令列表command = ['main.py', # 主脚本'--name=邮件群发工具', # 程序名称'--windowed', # 使用窗口模式'--onefile', # 打包成单个文件'--icon=email.png', # 程序图标'--version-file=version_info.txt', # 版本信息'--add-data=config.ini;.', # 配置文件'--add-data=README.md;.', # 说明文档'--add-data=email.png;.', # 图标文件'--clean', # 清理临时文件'--noconfirm', # 不询问确认'--uac-admin', # 请求管理员权限'--noupx', # 不使用UPX压缩f'--workpath=build', # 指定构建目录f'--distpath=dist', # 指定输出目录'--hidden-import=PyQt5.sip', # 添加隐式导入'--hidden-import=PyQt5.QtCore','--hidden-import=PyQt5.QtGui','--hidden-import=PyQt5.QtWidgets','--hidden-import=lxml._elementpath', # 添加lxml依赖'--hidden-import=lxml.etree', # 添加lxml依赖'--collect-all=lxml', # 收集所有lxml相关文件'--exclude-module=PyQt6', # 排除PyQt6'--exclude-module=PySide6', # 排除PySide6'--exclude-module=PySide2', # 排除PySide2]# 添加PyQt5依赖qt_path = os.path.join(os.path.dirname(PyQt5.__file__), 'Qt5')if os.path.exists(qt_path):# 添加Qt5的bin目录bin_path = os.path.join(qt_path, 'bin')if os.path.exists(bin_path):command.append(f'--add-data={bin_path};PyQt5/Qt5/bin')# 添加Qt5的plugins目录plugins_path = os.path.join(qt_path, 'plugins')if os.path.exists(plugins_path):command.append(f'--add-data={plugins_path};PyQt5/Qt5/plugins')# 添加qt_material资源try:import qt_materialqt_material_path = os.path.dirname(qt_material.__file__)resources_path = os.path.join(qt_material_path, 'resources')if os.path.exists(resources_path):command.append(f'--add-data={resources_path};qt_material/resources')except ImportError:print("警告: qt_material模块未找到")# 添加python-docx依赖try:import docxdocx_path = os.path.dirname(docx.__file__)command.append(f'--add-data={docx_path};docx')except ImportError:print("警告: python-docx模块未找到")# 运行构建命令PyInstaller.__main__.run(command)def main():"""主函数"""try:print("开始构建应用...")print("1. 清理旧文件...")clean_dist()clean_build()print("2. 创建版本信息...")create_version_info()print("3. 构建可执行文件...")os.environ['PYTHONPATH'] = os.path.dirname(os.path.abspath(__file__)) # 设置PYTHONPATHbuild_executable()print("4. 复制资源文件...")copy_resources()print("5. 清理临时文件...")if os.path.exists('version_info.txt'):os.remove('version_info.txt')print("构建完成!输出目录: dist/")except Exception as e:print(f"构建失败: {str(e)}")import tracebacktraceback.print_exc()sys.exit(1)if __name__ == "__main__":main()