python代码封装成可执行文件

首先确保python源码可以成功运行
之后执行命令如下
python -m pip install pyinstaller
先使用命令下载安装pyinstaller打包工具
之后直接在脚本所在的目录下执行打包命令(打包成单个exe文件)
pyinstaller -F -w -i icon.ico your_script_name.py

-F:打包成单个可执行文件(方便分发)
-w:不显示控制台窗口(GUI 程序必备)
-i icon.ico:设置程序图标(可选,需提前准备.ico 格式图标)
your_script_name.py:你的脚本文件名(比如email_sender.py)
执行成功后会在目录中生成个dist文件
将dist文件夹中的 exe 文件(及所需配置文件)分发给用户,双击即可运行,无需安装 Python 或任何依赖!

注意事项
1. 运行路径问题:打包后的 exe 会在自身所在目录读写文件(如email_config.json、secret.key),需确保这些文件与 exe 同目录
2. 杀毒软件误报:部分杀毒软件可能误报打包后的 exe,可添加信任
3. 兼容性:在 32 位系统打包的 exe 可在 32/64 位系统运行,64 位系统打包的 exe 仅能在 64 位系统运行


我的python代码如下:

import sys
import smtplib
import time
import datetime
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email.utils import formataddr
import json
import os
from cryptography.fernet import Fernet
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QLineEdit, QTextEdit, QPushButton, QLabel, QFileDialog,
QComboBox, QDateTimeEdit, QCheckBox, QTableWidget,
QTableWidgetItem, QMessageBox, QGroupBox, QSpinBox)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt6.QtGui import QFont
import pandas as pd

# -------------------------- 工具类 --------------------------
class EmailConfig:
"""邮箱配置管理(加密保存)"""
CONFIG_FILE = "email_config.json"
KEY_FILE = "secret.key"

@classmethod
def generate_key(cls):
"""生成加密密钥"""
if not os.path.exists(cls.KEY_FILE):
key = Fernet.generate_key()
with open(cls.KEY_FILE, "wb") as f:
f.write(key)
return open(cls.KEY_FILE, "rb").read()

@classmethod
def encrypt(cls, data):
"""加密数据"""
key = cls.generate_key()
fernet = Fernet(key)
return fernet.encrypt(data.encode()).decode()

@classmethod
def decrypt(cls, encrypted_data):
"""解密数据"""
key = cls.generate_key()
fernet = Fernet(key)
return fernet.decrypt(encrypted_data.encode()).decode()

@classmethod
def save_config(cls, sender_email, password, smtp_server, smtp_port):
"""保存配置"""
config = {
"sender_email": sender_email,
"password": cls.encrypt(password),
"smtp_server": smtp_server,
"smtp_port": smtp_port
}
with open(cls.CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2)

@classmethod
def load_config(cls):
"""加载配置"""
if not os.path.exists(cls.CONFIG_FILE):
return None
with open(cls.CONFIG_FILE, "r", encoding="utf-8") as f:
config = json.load(f)
config["password"] = cls.decrypt(config["password"])
return config

class EmailSender:
"""邮件发送核心类"""
@staticmethod
def create_email(sender, receiver, subject, content, is_html=False, attachments=None):
"""创建邮件对象"""
msg = MIMEMultipart()
# 发件人/收件人/主题
msg["From"] = formataddr(("自动邮件助手", sender))
msg["To"] = receiver if isinstance(receiver, str) else ",".join(receiver)
msg["Subject"] = Header(subject, "utf-8")

# 正文
msg.attach(MIMEText(content, "html" if is_html else "plain", "utf-8"))

# 附件
if attachments:
for file_path in attachments:
if os.path.exists(file_path):
with open(file_path, "rb") as f:
part = MIMEText(f.read(), "base64", "utf-8")
part["Content-Type"] = "application/octet-stream"
filename = os.path.basename(file_path)
part["Content-Disposition"] = f'attachment; filename="{filename}"'
msg.attach(part)
return msg

@staticmethod
def send_email(smtp_server, smtp_port, sender_email, password, receiver_email, subject, content, is_html=False, attachments=None):
"""发送单封邮件"""
try:
# 创建邮件
msg = EmailSender.create_email(sender_email, receiver_email, subject, content, is_html, attachments)

# 连接SMTP服务器
server = smtplib.SMTP_SSL(smtp_server, smtp_port) if smtp_port == 465 else smtplib.SMTP(smtp_server, smtp_port)
server.login(sender_email, password)

# 发送邮件(支持多个收件人)
receivers = [receiver_email] if isinstance(receiver_email, str) else receiver_email
server.sendmail(sender_email, receivers, msg.as_string())
server.quit()
return True, "发送成功"
except Exception as e:
return False, str(e)

# -------------------------- 线程类(避免UI卡死) --------------------------
class SendEmailThread(QThread):
"""邮件发送线程"""
finish_signal = pyqtSignal(bool, str) # 单个发送结果
log_signal = pyqtSignal(str, str, str) # 日志信号(时间、收件人、状态)
all_finish_signal = pyqtSignal() # 全部发送完成

def __init__(self, config, receivers, subject, content, is_html, attachments, delay=1):
super().__init__()
self.config = config # 邮箱配置
self.receivers = receivers # 收件人列表
self.subject = subject
self.content = content
self.is_html = is_html
self.attachments = attachments
self.delay = delay # 发送间隔(避免被邮箱服务器限流)
self.is_running = True

def stop(self):
self.is_running = False

def run(self):
for receiver in self.receivers:
if not self.is_running:
self.log_signal.emit(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), receiver, "发送取消")
break

success, msg = EmailSender.send_email(
smtp_server=self.config["smtp_server"],
smtp_port=self.config["smtp_port"],
sender_email=self.config["sender_email"],
password=self.config["password"],
receiver_email=receiver.strip(),
subject=self.subject,
content=self.content,
is_html=self.is_html,
attachments=self.attachments
)

log_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_status = f"成功: {msg}" if success else f"失败: {msg}"
self.log_signal.emit(log_time, receiver.strip(), log_status)

# 发送间隔
time.sleep(self.delay)

self.all_finish_signal.emit()

class TimerSendThread(QThread):
"""定时发送线程"""
timer_signal = pyqtSignal(str) # 倒计时信号
start_send_signal = pyqtSignal() # 开始发送信号

def __init__(self, target_time):
super().__init__()
self.target_time = target_time
self.is_running = True

def stop(self):
self.is_running = False

def run(self):
while self.is_running:
now = datetime.datetime.now()
diff = (self.target_time - now).total_seconds()
if diff <= 0:
self.start_send_signal.emit()
break

# 计算倒计时
hours = int(diff // 3600)
minutes = int((diff % 3600) // 60)
seconds = int(diff % 60)
self.timer_signal.emit(f"{hours:02d}:{minutes:02d}:{seconds:02d}")
time.sleep(1)

# -------------------------- 主窗口类 --------------------------
class AutoEmailSender(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("自动邮件发送助手")
self.setGeometry(100, 100, 1000, 700)
self.setMinimumSize(800, 600)

# 初始化变量
self.attachments = [] # 附件列表
self.receivers = [] # 收件人列表
self.send_thread = None
self.timer_thread = None

# 加载配置
self.config = EmailConfig.load_config()

# 初始化UI
self.init_ui()

def init_ui(self):
"""构建UI界面"""
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(15)
main_layout.setContentsMargins(20, 20, 20, 20)

# 1. 邮箱配置区域
config_group = QGroupBox("邮箱配置")
config_group.setFont(QFont("Arial", 11))
config_layout = QGridLayout(config_group)

# 标签和输入框
labels = ["发件人邮箱", "授权码/密码", "SMTP服务器", "SMTP端口"]
self.config_inputs = {}
smtp_servers = {
"QQ邮箱": ("smtp.qq.com", 465),
"163邮箱": ("smtp.163.com", 465),
"Gmail": ("smtp.gmail.com", 587),
"企业邮箱": ("smtp.exmail.qq.com", 465)
}

self.smtp_combo = QComboBox()
self.smtp_combo.addItems(smtp_servers.keys())
self.smtp_combo.currentTextChanged.connect(self.on_smtp_change)
self.port_input = QSpinBox()
self.port_input.setRange(1, 65535)

# 加载默认SMTP配置
default_smtp = "QQ邮箱"
self.smtp_combo.setCurrentText(default_smtp)
self.on_smtp_change(default_smtp)

# 添加到布局
for i, label_text in enumerate(labels):
label = QLabel(label_text)
label.setFont(QFont("Arial", 10))
config_layout.addWidget(label, i, 0, 1, 1)

if label_text == "SMTP服务器":
config_layout.addWidget(self.smtp_combo, i, 1, 1, 2)
elif label_text == "SMTP端口":
config_layout.addWidget(self.port_input, i, 1, 1, 2)
else:
input_box = QLineEdit()
input_box.setFont(QFont("Arial", 10))
if label_text == "授权码/密码":
input_box.setEchoMode(QLineEdit.EchoMode.Password)
self.config_inputs[label_text] = input_box
config_layout.addWidget(input_box, i, 1, 1, 2)

# 保存配置按钮
save_btn = QPushButton("保存配置")
save_btn.clicked.connect(self.save_config)
config_layout.addWidget(save_btn, 4, 1, 1, 2)

# 加载已保存的配置
if self.config:
self.config_inputs["发件人邮箱"].setText(self.config["sender_email"])
self.config_inputs["授权码/密码"].setText(self.config["password"])
self.smtp_combo.setCurrentText(self.get_smtp_name(self.config["smtp_server"]))
self.port_input.setValue(self.config["smtp_port"])

main_layout.addWidget(config_group)

# 2. 收件人区域
receiver_group = QGroupBox("收件人管理")
receiver_group.setFont(QFont("Arial", 11))
receiver_layout = QVBoxLayout(receiver_group)

# 收件人输入框和按钮
receiver_hbox = QHBoxLayout()
self.receiver_input = QLineEdit()
self.receiver_input.setPlaceholderText("输入单个收件人邮箱,或用逗号分隔多个邮箱")
self.receiver_input.setFont(QFont("Arial", 10))
add_receiver_btn = QPushButton("添加")
add_receiver_btn.clicked.connect(self.add_receiver)
import_btn = QPushButton("导入文件")
import_btn.clicked.connect(self.import_receivers)
clear_receiver_btn = QPushButton("清空")
clear_receiver_btn.clicked.connect(self.clear_receivers)

receiver_hbox.addWidget(self.receiver_input)
receiver_hbox.addWidget(add_receiver_btn)
receiver_hbox.addWidget(import_btn)
receiver_hbox.addWidget(clear_receiver_btn)

# 收件人列表
self.receiver_table = QTableWidget()
self.receiver_table.setColumnCount(1)
self.receiver_table.setHorizontalHeaderLabels(["已添加收件人"])
self.receiver_table.horizontalHeader().setStretchLastSection(True)
self.receiver_table.setFont(QFont("Arial", 10))

receiver_layout.addLayout(receiver_hbox)
receiver_layout.addWidget(self.receiver_table)
main_layout.addWidget(receiver_group)

# 3. 邮件内容区域
content_group = QGroupBox("邮件内容")
content_group.setFont(QFont("Arial", 11))
content_layout = QVBoxLayout(content_group)

# 主题
subject_hbox = QHBoxLayout()
subject_label = QLabel("主题:")
subject_label.setFont(QFont("Arial", 10))
self.subject_input = QLineEdit()
self.subject_input.setFont(QFont("Arial", 10))
subject_hbox.addWidget(subject_label)
subject_hbox.addWidget(self.subject_input)

# 正文
content_label = QLabel("正文:")
content_label.setFont(QFont("Arial", 10))
self.content_edit = QTextEdit()
self.content_edit.setFont(QFont("Arial", 10))
self.content_edit.setPlaceholderText("支持HTML格式(需勾选下方选项)")

# HTML选项
self.html_checkbox = QCheckBox("使用HTML格式")
self.html_checkbox.setFont(QFont("Arial", 10))

content_layout.addLayout(subject_hbox)
content_layout.addWidget(content_label)
content_layout.addWidget(self.content_edit)
content_layout.addWidget(self.html_checkbox)
main_layout.addWidget(content_group)

# 4. 附件区域
attachment_group = QGroupBox("附件管理")
attachment_group.setFont(QFont("Arial", 11))
attachment_layout = QVBoxLayout(attachment_group)

attachment_hbox = QHBoxLayout()
self.attachment_label = QLabel("未添加附件")
self.attachment_label.setFont(QFont("Arial", 10))
add_attach_btn = QPushButton("添加附件")
add_attach_btn.clicked.connect(self.add_attachment)
clear_attach_btn = QPushButton("清空附件")
clear_attach_btn.clicked.connect(self.clear_attachments)

attachment_hbox.addWidget(self.attachment_label)
attachment_hbox.addStretch()
attachment_hbox.addWidget(add_attach_btn)
attachment_hbox.addWidget(clear_attach_btn)

attachment_layout.addLayout(attachment_hbox)
main_layout.addWidget(attachment_group)

# 5. 发送控制区域
control_group = QGroupBox("发送控制")
control_group.setFont(QFont("Arial", 11))
control_layout = QHBoxLayout(control_group)

# 定时发送
self.timer_checkbox = QCheckBox("定时发送")
self.timer_checkbox.setFont(QFont("Arial", 10))
self.timer_checkbox.stateChanged.connect(self.toggle_timer)
self.timer_edit = QDateTimeEdit()
self.timer_edit.setDateTime(datetime.datetime.now())
self.timer_edit.setFont(QFont("Arial", 10))
self.timer_edit.setEnabled(False)
self.countdown_label = QLabel("倒计时:--:--:--")
self.countdown_label.setFont(QFont("Arial", 10, QFont.Weight.Bold))

# 发送间隔
delay_label = QLabel("发送间隔(秒):")
delay_label.setFont(QFont("Arial", 10))
self.delay_spin = QSpinBox()
self.delay_spin.setRange(1, 60)
self.delay_spin.setValue(3)
self.delay_spin.setFont(QFont("Arial", 10))

# 发送/停止按钮
self.send_btn = QPushButton("开始发送")
self.send_btn.clicked.connect(self.start_send)
self.send_btn.setFont(QFont("Arial", 10, QFont.Weight.Bold))
self.stop_btn = QPushButton("停止发送")
self.stop_btn.clicked.connect(self.stop_send)
self.stop_btn.setFont(QFont("Arial", 10, QFont.Weight.Bold))
self.stop_btn.setEnabled(False)

control_layout.addWidget(self.timer_checkbox)
control_layout.addWidget(self.timer_edit)
control_layout.addWidget(self.countdown_label)
control_layout.addStretch()
control_layout.addWidget(delay_label)
control_layout.addWidget(self.delay_spin)
control_layout.addSpacing(20)
control_layout.addWidget(self.send_btn)
control_layout.addWidget(self.stop_btn)

main_layout.addWidget(control_group)

# 6. 日志区域
log_group = QGroupBox("发送日志")
log_group.setFont(QFont("Arial", 11))
log_layout = QVBoxLayout(log_group)

self.log_table = QTableWidget()
self.log_table.setColumnCount(3)
self.log_table.setHorizontalHeaderLabels(["时间", "收件人", "状态"])
self.log_table.horizontalHeader().setStretchLastSection(True)
self.log_table.setFont(QFont("Arial", 10))

log_layout.addWidget(self.log_table)
main_layout.addWidget(log_group, 1) # 占剩余空间

def on_smtp_change(self, smtp_name):
"""SMTP服务器选择变化"""
smtp_servers = {
"QQ邮箱": ("smtp.qq.com", 465),
"163邮箱": ("smtp.163.com", 465),
"Gmail": ("smtp.gmail.com", 587),
"企业邮箱": ("smtp.exmail.qq.com", 465)
}
server, port = smtp_servers[smtp_name]
self.port_input.setValue(port)

def get_smtp_name(self, server):
"""根据服务器地址获取SMTP名称"""
smtp_servers = {
"smtp.qq.com": "QQ邮箱",
"smtp.163.com": "163邮箱",
"smtp.gmail.com": "Gmail",
"smtp.exmail.qq.com": "企业邮箱"
}
return smtp_servers.get(server, "企业邮箱")

def save_config(self):
"""保存邮箱配置"""
sender_email = self.config_inputs["发件人邮箱"].text().strip()
password = self.config_inputs["授权码/密码"].text().strip()
smtp_server = {
"QQ邮箱": "smtp.qq.com",
"163邮箱": "smtp.163.com",
"Gmail": "smtp.gmail.com",
"企业邮箱": "smtp.exmail.qq.com"
}[self.smtp_combo.currentText()]
smtp_port = self.port_input.value()

if not sender_email or not password:
QMessageBox.warning(self, "警告", "发件人邮箱和密码不能为空!")
return

EmailConfig.save_config(sender_email, password, smtp_server, smtp_port)
self.config = EmailConfig.load_config()
QMessageBox.information(self, "成功", "配置保存成功!")

def add_receiver(self):
"""添加收件人"""
receiver_text = self.receiver_input.text().strip()
if not receiver_text:
QMessageBox.warning(self, "警告", "请输入收件人邮箱!")
return

# 支持逗号分隔多个邮箱
receivers = [r.strip() for r in receiver_text.split(",") if r.strip()]
for receiver in receivers:
if receiver not in self.receivers:
self.receivers.append(receiver)
self.update_receiver_table()

self.receiver_input.clear()

def import_receivers(self):
"""导入收件人文件(TXT/Excel)"""
file_path, _ = QFileDialog.getOpenFileName(self, "选择收件人文件", "", "文件 (*.txt *.xlsx *.xls)")
if not file_path:
return

try:
receivers = []
if file_path.endswith(".txt"):
with open(file_path, "r", encoding="utf-8") as f:
receivers = [line.strip() for line in f if line.strip()]
else: # Excel
df = pd.read_excel(file_path)
receivers = df.iloc[:, 0].dropna().astype(str).tolist()

# 去重并添加
new_receivers = [r for r in receivers if r not in self.receivers]
self.receivers.extend(new_receivers)
self.update_receiver_table()
QMessageBox.information(self, "成功", f"导入成功!新增 {len(new_receivers)} 个收件人")
except Exception as e:
QMessageBox.warning(self, "错误", f"导入失败:{str(e)}")

def update_receiver_table(self):
"""更新收件人表格"""
self.receiver_table.setRowCount(len(self.receivers))
for i, receiver in enumerate(self.receivers):
item = QTableWidgetItem(receiver)
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.receiver_table.setItem(i, 0, item)

def clear_receivers(self):
"""清空收件人"""
self.receivers.clear()
self.update_receiver_table()

def add_attachment(self):
"""添加附件"""
file_paths, _ = QFileDialog.getOpenFileNames(self, "选择附件", "", "所有文件 (*.*)")
if not file_paths:
return

self.attachments.extend([f for f in file_paths if f not in self.attachments])
self.update_attachment_label()

def update_attachment_label(self):
"""更新附件标签"""
if not self.attachments:
self.attachment_label.setText("未添加附件")
else:
self.attachment_label.setText(f"已添加 {len(self.attachments)} 个附件:{', '.join([os.path.basename(f) for f in self.attachments[:3]])}{'...' if len(self.attachments) > 3 else ''}")

def clear_attachments(self):
"""清空附件"""
self.attachments.clear()
self.update_attachment_label()

def toggle_timer(self):
"""切换定时发送状态"""
self.timer_edit.setEnabled(self.timer_checkbox.isChecked())

def start_send(self):
"""开始发送邮件"""
# 验证配置
if not self.config:
QMessageBox.warning(self, "警告", "请先配置并保存邮箱信息!")
return

# 验证收件人
if not self.receivers:
QMessageBox.warning(self, "警告", "请添加至少一个收件人!")
return

# 验证邮件内容
subject = self.subject_input.text().strip()
content = self.content_edit.toPlainText().strip()
if not subject or not content:
QMessageBox.warning(self, "警告", "主题和正文不能为空!")
return

# 定时发送
if self.timer_checkbox.isChecked():
target_time = self.timer_edit.dateTime().toPyDateTime()
now = datetime.datetime.now()
if target_time <= now:
QMessageBox.warning(self, "警告", "定时时间必须晚于当前时间!")
return

# 启动定时线程
self.timer_thread = TimerSendThread(target_time)
self.timer_thread.timer_signal.connect(self.update_countdown)
self.timer_thread.start_send_signal.connect(self.do_send)
self.timer_thread.start()
self.send_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
QMessageBox.information(self, "提示", f"定时发送已设置!将在 {target_time.strftime('%Y-%m-%d %H:%M:%S')} 开始发送")
else:
# 立即发送
self.do_send()

def do_send(self):
"""执行发送(真正的发送逻辑)"""
# 构建配置
send_config = {
"sender_email": self.config["sender_email"],
"password": self.config["password"],
"smtp_server": self.config["smtp_server"],
"smtp_port": self.config["smtp_port"]
}

# 获取邮件信息
subject = self.subject_input.text().strip()
content = self.content_edit.toPlainText().strip()
is_html = self.html_checkbox.isChecked()
delay = self.delay_spin.value()

# 启动发送线程
self.send_thread = SendEmailThread(send_config, self.receivers, subject, content, is_html, self.attachments, delay)
self.send_thread.log_signal.connect(self.add_log)
self.send_thread.all_finish_signal.connect(self.send_finish)
self.send_thread.start()

# 更新按钮状态
self.send_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self.countdown_label.setText("发送中...")

def stop_send(self):
"""停止发送"""
if self.timer_thread and self.timer_thread.isRunning():
self.timer_thread.stop()
self.timer_thread.wait()
self.countdown_label.setText("倒计时:--:--:--")

if self.send_thread and self.send_thread.isRunning():
self.send_thread.stop()
self.send_thread.wait()

self.send_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.add_log(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "系统", "发送已取消")

def send_finish(self):
"""发送完成"""
self.send_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.countdown_label.setText("发送完成!")
QMessageBox.information(self, "提示", "所有邮件发送完成!")

def add_log(self, time_str, receiver, status):
"""添加发送日志"""
row = self.log_table.rowCount()
self.log_table.insertRow(row)

# 时间
time_item = QTableWidgetItem(time_str)
time_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.log_table.setItem(row, 0, time_item)

# 收件人
receiver_item = QTableWidgetItem(receiver)
receiver_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.log_table.setItem(row, 1, receiver_item)

# 状态(成功绿色,失败红色)
status_item = QTableWidgetItem(status)
if "成功" in status:
status_item.setForeground(Qt.GlobalColor.green)
else:
status_item.setForeground(Qt.GlobalColor.red)
status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.log_table.setItem(row, 2, status_item)

# 滚动到最后一行
self.log_table.scrollToBottom()

def update_countdown(self, countdown_str):
"""更新倒计时"""
self.countdown_label.setText(f"倒计时:{countdown_str}")

def closeEvent(self, event):
"""关闭窗口时停止线程"""
if self.send_thread and self.send_thread.isRunning():
self.send_thread.stop()
self.send_thread.wait()
if self.timer_thread and self.timer_thread.isRunning():
self.timer_thread.stop()
self.timer_thread.wait()
event.accept()

# -------------------------- 主函数 --------------------------
if __name__ == "__main__":
app = QApplication(sys.argv)
window = AutoEmailSender()
window.show()
sys.exit(app.exec())

以上是一个邮件群发系统,方便同事联系客户,进行群发

posted @ 2025-12-01 14:02  张云鹏鹏  阅读(0)  评论(0)    收藏  举报