基于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 或更高版本

快速开始

  1. 克隆项目
git clone [https://github.com/liugang926/Auto-mail-sent.git]
cd email-sender
  1. 创建虚拟环境(推荐)
python -m venv venv
# Windows
venv\Scripts\activate
# Linux/Mac
source venv/bin/activate
  1. 安装依赖
pip install -r requirements.txt
  1. 运行程序
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数据读取

打包说明

环境准备

  1. 安装PyInstaller
pip install pyinstaller
  1. 确保所需资源文件存在:
  • config.ini(邮箱配置文件)
  • email.png(程序图标)
  • README.md(说明文档)

打包步骤

  1. 运行打包脚本
python setup.py
  1. 打包过程说明:
  • 清理旧的构建文件
  • 创建版本信息
  • 构建可执行文件
  • 复制必要资源
  • 清理临时文件
  1. 打包完成后,在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表格的列名完全一致
  • 支持任意数量的变量

示例:

使用指南

  1. 准备工作

    • 创建Word邮件模板
    • 准备Excel收件人数据
    • 配置config.ini文件
  2. 启动程序

    python main.py
    
  3. 操作步骤

    • 选择Word模板文件
    • 选择Excel数据文件
    • 选择姓名和邮箱列
    • 填写邮件主题
    • 设置发送间隔
    • 测试邮箱配置
    • 生成预览确认
    • 开始发送

注意事项

  1. 发送前检查事项:

    • 确保网络连接正常
    • 验证邮箱配置正确
    • 检查模板格式无误
    • 确认收件人数据完整
  2. 发送建议:

    • 首次使用建议先测试配置
    • 大量发送时适当增加间隔
    • 定期检查发送状态
    • 注意邮件服务商限制

常见问题解决

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

技术支持

如遇问题,请按以下步骤处理:

  1. 检查配置文件设置
  2. 查看程序运行日志
  3. 确认网络连接状态
  4. 提交Issue或联系技术支持

版本历史

  • v1.0.0
    • 基础邮件发送功能
    • Word模板和Excel数据支持
    • 现代化UI界面
    • 邮箱配置测试
    • 打包功能支持
    • 邮件和姓名以及内容的其他变量自动匹配

许可说明

本项目仅供学习和参考使用。在使用本工具时,请遵守:

  1. 相关法律法规
  2. 邮件服务商的使用规范
  3. 用户隐私保护规定
### 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} &lt;{self.email_sender.sender_email}&gt;</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() 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/74261.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大模型-提示词(Prompt)技巧

1、什么是提示词&#xff1f; 提示词&#xff08;Prompt&#xff09;是用户发送给大语言模型的问题、指令或请求&#xff0c;用来明确地告诉模型用户想要解决的问题或完成的任务&#xff0c;是大语言模型理解用户需求并据此生成相关、准确回答或内容的基础。对于大语言模型来说…

Android开发:support.v4包与AndroidX

Android中的support.v4包与AndroidX support.v4包概述 Android Support Library中的android.support.v4包是Google为保持Android应用向后兼容而提供的重要支持库集合。它主要解决以下问题&#xff1a; API版本兼容&#xff1a;让新版API能在旧版Android系统上使用功能增强&a…

TCP-IP模型

书接上回&#xff08;OSI通信模型&#xff09; TCP-IP协议结构 &#xff08;略讲&#xff09; ARP&#xff1a;IP-->MAC RARP&#xff1a;MAC-->IP ICMP&#xff1a;控制报文信息协议&#xff0c;主要是涉及到主机就去连接路由器时控制传输报文&#xff08…

雪花算法生成的主键存在哪些问题,为什么不能使用自增ID或者UUID做MySQL的主键

MySQL 分布式架构中的主键选择&#xff1a;自增ID、UUID与雪花算法 为什么MySQL分布式架构中不能使用自增主键&#xff1f; 在分布式架构中&#xff0c;自增主键存在以下问题&#xff1a; 主键冲突风险&#xff1a;多个数据库实例同时生成自增主键会导致ID重复分片不均匀&am…

RapidJSON 处理 JSON(高性能 C++ 库)(四)

第四部分:RapidJSON 处理 JSON(高性能 C++ 库) 📢 快速掌握 JSON!文章 + 视频双管齐下 🚀 如果你觉得阅读文章太慢,或者更喜欢 边看边学 的方式,不妨直接观看我录制的 RapidJSON 课程视频!🎬 视频里会用更直观的方式讲解 RapidJSON 的核心概念、实战技巧,并配有…

chromem-go + ollama + bge-m3 进行文档向量嵌入和查询

Ollama 安装 https://ollama.com/download Ollama 运行嵌入模型 bge-m3:latest ollama run bge-m3:latestchromem-go 文档嵌入和查询 package mainimport ("context""fmt""runtime""github.com/philippgille/chromem-go" )func ma…

【LeetCode 题解】数据库:180. 连续出现的数字

一、问题描述 给定一个Logs表&#xff0c;包含自增 ID 和数字字段&#xff1a; CREATE TABLE Logs (id INT PRIMARY KEY AUTO_INCREMENT,num VARCHAR(50) );要求编写 SQL 查询&#xff0c;找出所有至少连续出现三次的数字。例如&#xff1a; --------- | id | num | -------…

MaxEnt模型进阶:基于R语言自动化与GIS空间建模的物种栖息地精准预测

生物多样性的空间分布规律及其对环境变化的响应机制&#xff0c;是生态学与地理学研究的前沿议题。在气候变化加剧和人类活动干扰的双重压力下&#xff0c;如何精准预测物种潜在分布范围、识别关键环境驱动因子&#xff0c;已成为制定生物保护策略的核心科学问题。物种分布模型…

缓存雪崩解决方案:二级缓存VS随机TTL

背景 在学习缓存雪崩的时候&#xff0c;了解到有二级缓存和随机TTL两个解决方案&#xff0c;但是在学习之后&#xff0c;个人认为二级缓存本质上还是利用两层缓存的过期时间不一致来实现缓存过期时间随机化&#xff0c;这不就是和随机TTL一样吗&#xff0c;故有了这篇思考&…

Android View事件分发机制深度解析

在Android面试中&#xff0c;关于View事件分发机制的考察往往不仅限于基础流程&#xff0c;更关注底层原理、性能优化和实际应用场景。以下是针对面试的全面回答策略&#xff1a; 一、基础回答框架 核心三要素&#xff1a; 传递流程 "事件分发遵循Activity → Window →…

2829. k-avoiding 数组的最小总和

2829. k-avoiding 数组的最小总和 题目链接&#xff1a;2829. k-avoiding 数组的最小总和 代码如下&#xff1a; class Solution { public:int minimumSum(int n, int k) {int m min(k / 2, n);return (m * (m 1) (k * 2 n - m - 1) * (n - m)) / 2;} };

phpStorm2021.3.3在windows系统上配置Xdebug调试

开始 首先根据PHP的版本下载并安装对应的Xdebug扩展在phpStorm工具中找到设置添加服务添加php web page配置完信息后 首先根据PHP的版本下载并安装对应的Xdebug扩展 我使用的是phpStudy工具&#xff0c;直接在php对应的版本中开启xdebug扩展&#xff0c; 并在php.ini中添加如下…

LabVIEW永磁同步电机性能测试系统

开发了一种基于LabVIEW的永磁同步电机&#xff08;PMSM&#xff09;性能测试系统的设计及应用。该系统针对新能源汽车使用的电机进行稳态性能测试&#xff0c;解决了传统测试方法成本高、效率低的问题&#xff0c;实现了测试自动化&#xff0c;提高了数据的准确性和客观性。 ​…

谷粒商城:Redisson

目录 Redisson 整合Redisson RLock RReadWriteLock RSemaphore RCountDownLatch 优化三级分类缓存 缓存一致性问题 双写模式 失效模式 脏数据解决 Redisson 提供redis分布式锁&#xff08;Distributed locks with Redis&#xff09;的java客户端 整合Redisson 引入 …

Linux系统调用编程

目录 一. 理解进程和线程的概念。并在Linux系统下进行相应操作 1.1概念 1.1.1进程(Process) 1.1.2 线程(Thread) 1.2操作 1.2.1用 ps -a 命令查看系统中各进程的编号pid 1.2.2用kill 命令终止一个进程pid 二. 解释Linux的“虚拟内存管理”&#xff0c;它与stm32中的 真…

25-智慧旅游系统(协同算法)三端

介绍 技术&#xff1a; 基于 B/S 架构 SpringBootMySQLLayuivue 环境&#xff1a; Idea mysql maven jdk1.8 node 管理端功能 首页展示图表&#xff1a;以数据可视化方式展示关键业务数据。 用户管理&#xff1a;管理系统用户&#xff0c;包括查看、编辑等操作。 供应商管…

【stm32--HAL库DMA+USART+空闲中断不定长收发数据】

串口通信-Hal库实现不定长度收发&#xff0c;DMAUSART DMA串口STM32CUBEMX配置&#xff08;工程创建&#xff09;基础配置时钟配置工程配置 代码编写现象 DMA 在正式配置之前&#xff0c;我们先来一起简单了解一下DMA。DMA&#xff08;Direct Memory Access&#xff0c;直接内…

沉浸式体验测评|AI Ville:我在Web3小镇“生活”了一周

最近&#xff0c;我在朋友的推荐下&#xff0c;体验了 aivillebot 的项目。起初&#xff0c;我只是抱着试试看的心态&#xff0c;心想这不就是个 Web3 版的《星露谷物语》吗&#xff1f; 但是一周下来&#xff0c;我发现这个虚拟小镇也没那么简单——里面的居民不是目前端游或链…

FPGA学习-基于 DE2-115 板的 Verilog 分秒计数器设计与按键功能实现

一、核心功能设计 按键暂停/继续&#xff1a;通过KEY1控制计时状态 按键消抖处理&#xff1a;20ms消抖周期消除机械抖动 硬件资源分配&#xff1a;符合DE2-115开发板引脚规范 二、核心模块实现详解 1. 顶层模块&#xff08;counter&#xff09; module counter(input CL…

后端开发 SpringBoot 工程模板

概述 本篇文章主要记录如何开发一个通用的 SpringBoot 工程开发框架的项目模板&#xff0c;这样后续需要开发项目时就可以直接开箱直用了&#xff0c;省区了很多重复步骤。 项目初始化 创建项目&#xff1a; 按照我的选项来选&#xff0c;然后点击 create&#xff0c;等待文…