基于python开发的邮箱合并群发工具
智能邮件群发系统
一个基于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 QApplication
class 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_sender
self.data = data
self.template = template
self.variable_columns = variable_columns
self.subject = subject
self.interval = interval
self.is_running = True
def run(self):
total = len(self.data)
for i, row in enumerate(self.data):
if not self.is_running:
break
try:
# 替换所有变量
content = self.template
for 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 = False
class 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 = None
self.name_column = ""
self.email_column = ""
# 先创建UI
self.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 = None
self.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 = col
if 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:
return
if 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模板!")
return
if not self.excel_data:
QMessageBox.warning(self, "警告", "请先加载Excel数据!")
return
self.name_column = self.name_column
self.email_column = self.email_column
if not self.name_column or not self.email_column:
QMessageBox.warning(self, "警告", "请选择姓名和邮箱列!")
return
subject = 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_name
sender_email = self.email_sender.sender_email
smtp_server = self.email_sender.smtp_server
smtp_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_email
msg['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 os
class 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 config
def send_email(self, to_email, subject, html_content):
"""发送邮件"""
# 创建邮件
msg = MIMEMultipart('alternative')
msg['From'] = f"{self.sender_name} <{self.sender_email}>"
msg['To'] = to_email
msg['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 Document
class 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}")
# 转换为HTML
html_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 os
class ExcelReader:
"""Excel文件读取器"""
def read_data(self, file_path):
"""
读取Excel文件数据
返回数据列表和列名列表
"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"找不到文件: {file_path}")
try:
# 读取Excel
df = pd.read_excel(file_path)
# 验证数据帧不为空
if df.empty:
raise ValueError("Excel文件中没有数据")
# 将数据转换为列表字典
data = df.to_dict(orient='records')
# 获取列名
columns = df.columns.tolist()
return data, columns
except 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 PyQt5
def 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:
pass
def 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_material
qt_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 docx
docx_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__)) # 设置PYTHONPATH
build_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 traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()