因工作需要做一个自动发送邮件的功能,要求是周末定时发送。
原因是这样,公司的行政办公人员周末和节假日不上班,但总有一些人加班,而公司是采用报餐的形式去报中午的餐数量,
平时都是行政人员发数据给中央厨房,而周末节日行政人员不上班,厨房又没有配置电脑这些,而且厨房不一定在公司附近,
可能距离比较远。所以有了这样的需求,总结就是:
1:周末,节日自动定时发送邮件给中央厨房。邮件内容含当天的用餐早报人数
2:厨房没有办法连接到公司的网络,也没有配备电脑。
那我们开始设计思路:
做一个自动发送邮件,邮件正文含当天的报餐人数给厨房就行了。那么我们要解决的有以下两点
一:从公司的报餐数据库中取出当日的报餐数量,汇总后以正文的形式发一封邮件给厨房
二:定时发送给指定邮箱的人。
考虑到以上可能数据库会变,邮件接收人会更换等。应该做成可配置灵活的,而不是写死。
基于保密,数据库连接密码,邮件授权码这些就做成加密的形式保存。
先看一下UI布局的整体效果。
我用的是PySimpleGUI这个库。
先导入要用到的库,如果没有的自己去:pip install XXXXXX
一:程序中用到的库如下:
import time as t
import PySimpleGUI as sg
import smtplib
import os
import pyodbc
import _thread
from configparser import ConfigParser
from datetime import datetime,time
import GetDinData
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import base64
from Crypto.Cipher import AES
二:
界面UI布局及主题
sg.theme('GreenMono') # 设置当前主题
布局设置
layout = [
#收件信息控件布置
[sg.Text('收件人: ',font=('微软雅黑', 12)), sg.InputText('', key='_To',size=(70, 1), font=('微软雅黑', 12))],
[sg.Text('抄送:\t',font=('微软雅黑', 12)), sg.InputText('', key='_To_copy',size=(70, 1), font=('微软雅黑', 12))],
[sg.Text('主题:\t',font=('微软雅黑', 12)), sg.InputText('理研-报/用餐数据汇总及明细表', key='_Subject',size=(70, 1), font=('微软雅黑', 12))],
[sg.Text('附件:\t',font=('微软雅黑', 12)),
sg.FileBrowse('添加附件', file_types=(('Text Files', '. '),), size=(10, 1), font=('微软雅黑', 11),key='_OpenFile'),
sg.Button('删除附件', font=('微软雅黑', 12),key='_Delete')
],
#正文
[sg.Frame(layout=[
[sg.Multiline(default_text='您好:'+'\n'+' 这是您申请发送的(报/用餐数据汇总)情况,请您查看! 注:此邮件为自动发送邮件!' + '\n',
size=(63, 10),font=('微软雅黑', 14), text_color='Blue', key='_Content',auto_size_text=True)],
],
title='邮件正文', title_color='red', relief=sg.RELIEF_SUNKEN, tooltip='邮件正文')
],
服务器及发件人控件布置
[sg.Frame(layout=[
[sg.Text('服务器: ', font=('微软雅黑', 12), text_color='red'),
sg.InputText('smtp.qq.com', key='_Server', size=(25, 1), font=('微软雅黑', 12)),
sg.Text('端口号: ', font=('微软雅黑', 12), text_color='red'),
sg.InputText('465', key='_Port', size=(5, 1), font=('微软雅黑', 12)),
sg.Text('授权码: ',font=('微软雅黑', 12), text_color='red'),
sg.InputText('',key='_AutoCode', size=(18, 1), font=('微软雅黑', 12)), ],
[sg.Text('发件人: ', font=('微软雅黑', 12), text_color='red'),
sg.InputText('', key='_From', size=(69, 1), font=('微软雅黑', 12))],
[sg.Text('定时1 : ', font=('微软雅黑', 12),text_color='blue'),sg.InputText('10:00', key='_AutoTime1',size=(9, 1)),
sg.Text('定时2 : ', font=('微软雅黑', 12),text_color='blue'),sg.InputText('15:00', key='_AutoTime2',size=(9, 1)),
sg.Button('邮件服务器连接测试', font=('微软雅黑', 12),button_color='blue', key='_ConnectEmail'),
sg.Button('显示配置信息', font=('微软雅黑', 12),key='_ShowAutoCode'),
sg.Button('保存配置', font=('微软雅黑', 12),key='_SaveConfigEmail')],
],
title='(1): 邮件服务器及发件人配置', title_color='red', relief=sg.RELIEF_SUNKEN, tooltip='邮件服务器及发件人设置')
],
# 服务器数据库控件布置
[sg.Frame(layout=[
[sg.Text('服务器地址:', font=('微软雅黑', 12), text_color='red'),
sg.InputText('', key='_ServerName', size=(30, 1), font=('微软雅黑', 12), tooltip='服务名或IP地址'),
sg.Text('数据库名称:', font=('微软雅黑', 12), text_color='red'),
sg.InputText('', key='_Database', size=(25, 1), font=('微软雅黑', 12), tooltip='数据库名称')],
[sg.Text('登录用户名:', font=('微软雅黑', 12), text_color='red'),
sg.InputText('sa', key='_LogId', size=(30, 1), font=('微软雅黑', 12), tooltip='正常是sa'),
sg.Text('用户名密码:', font=('微软雅黑', 12), text_color='red'),
sg.InputText('',key='_LogPassword', size=(25, 1), font=('微软雅黑', 12), tooltip='sa对应的密码')],
[sg.Text('操作按钮 >>>>>>>>>>>>>>>', text_color='blue', font=('微软雅黑', 12)),
sg.Button('数据库连接测试', font=('微软雅黑', 12), button_color='blue', key='_ConnectDB'),
sg.Button('显示配置信息', font=('微软雅黑', 12), key='_ShowConfig'),
sg.Button('保存配置', font=('微软雅黑', 12), key='_SaveConfigDB'),
sg.Button('测试获取数据', font=('微软雅黑', 12), key='_Get'),
],
],
title='(2): 服务器数据库配置', title_color='red', relief=sg.RELIEF_SUNKEN, tooltip='服务器数据库设置')
],
[sg.Text('说明: (1) 多个收件人地址用分号";"隔开 (2) 定时周末及节假日才会发送',text_color='red',font=('微软雅黑', 12)),
sg.Button('发送邮件', font=('微软雅黑', 12),key='_Send'),
sg.Button('开启自动发送', font=('微软雅黑', 12),key='_AutoSend')],
[sg.Text('运行状态: 还未开启自动发送邮件',text_color='yellow', font=('微软雅黑', 12),key='_State')],
# [sg.StatusBar('运行状态: 还未开启自动发送邮件',text_color='yellow', font=('微软雅黑', 12),key='_State')]
]
创建窗口
window = sg.Window('定时自动发送邮件小程序,Author:Running Ver:1.0 ; 程序运行时间: ' + t.strftime('%Y-%m-%d %H:%M:%S'), layout,font=('微软雅黑', 12), default_element_size=(50, 1))
三:配置文件及相关
配置文件
app_path = 'setting.ini'
log_path = 'log.txt'
key = 'lsqily82lsqily82' #自己密钥一定要16字节不然会报错
控制是否点了"显示授权码"默认为True
AutoFlag = False
四:AES加密及解密
def AES_Encrypt(key, data):
密钥(key), 密斯偏移量(iv) CBC模式加密
vi = '0102030405060708'
pad = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16)
data = pad(data)
字符串补位
cipher = AES.new(key.encode('utf8'), AES.MODE_CBC, vi.encode('utf8'))
encryptedbytes = cipher.encrypt(data.encode('utf8'))
加密后得到的是bytes类型的数据
encodestrs = base64.b64encode(encryptedbytes)
使用Base64进行编码,返回byte字符串
enctext = encodestrs.decode('utf8')
对byte字符串按utf-8进行解码
return enctext
def AES_Decrypt(key, data):
vi = '0102030405060708'
data = data.encode('utf8')
encodebytes = base64.decodebytes(data)
将加密数据转换位bytes类型数据
cipher = AES.new(key.encode('utf8'), AES.MODE_CBC, vi.encode('utf8'))
text_decrypted = cipher.decrypt(encodebytes)
unpad = lambda s: s[0:-s[-1]]
text_decrypted = unpad(text_decrypted)
去补位
text_decrypted = text_decrypted.decode('utf8')
return text_decrypted
五:邮件服务器配置相关函数-
----------------邮件服务器配置相关函数--------------------
定义全局变量
Server=''
def ini():
"初始化创建自动发邮件配置文件"
创建一个ConfigParser对象
config = ConfigParser()
if not os.path.exists(app_path):
sg.popup('没有'+app_path+'配置文件,请先在当前界面配置邮件服务器并保存配置文件', title='提示', )
return 'N'
else:
return 'Y'
def Data_Effect():
"数据有效性验证"
info='OK'
#获取UI界面上的各项值
ToMail = values['_To'] # 收件人昵称和地址
Subject = values['_Subject'] # 邮件主题
Smtp = values['_Server']
Port = values['_Port']
AutoCode = values['_AutoCode']
SendAddress = values['_From']
AutoTime1 = values['_AutoTime1']
AutoTime2 = values['_AutoTime2']
Server = values['_ServerName']
DataBase = values['_Database']
LogId = values['_LogId']
LogPassword = values['_LogPassword']
#判断是否为空
if len(ToMail)0:
info="收件人邮件地址不能为空"
sg.popup('收件人邮件地址不能为空', title='提示', )
elif len(Subject)0:
sg.popup('邮件主题不能为空', title='提示', )
elif len(Smtp)0:
sg.popup('邮件服务器不能为空', title='提示', )
elif len(Port)0:
sg.popup('端口号不能为空', title='提示', )
elif len(AutoCode)0:
sg.popup('授权码不能为空', title='提示', )
elif len(AutoTime1)0:
sg.popup('定时1不能为空', title='提示', )
elif len(AutoTime2)0:
sg.popup('定时2不能为空', title='提示', )
elif len(Server)0:
sg.popup('服务器地址不能为空', title='提示', )
elif len(DataBase)0:
sg.popup('数据库名称不能为空', title='提示', )
elif len(LogId)0:
sg.popup('登录用户名不能为空', title='提示', )
elif len(LogPassword)0:
sg.popup('用户名密码不能为空', title='提示', )
window['_State'].update('当前状态: '+ info)
return info
def show_ini():
#显示配置文件前,先判断是否有生成配置文件
if ini()'N':
return
创建一个ConfigParser对象
config = ConfigParser()
try:
从 INI文件中读取配置信息
config.read(app_path, encoding="utf-8")
获取指定节点下的键值对
Smtp = config['SMTP']['Smtp']
Port = config['SMTP']['Port']
AutoCode = config['SMTP']['AutoCode']
显示出解密的明文 ,授权码jyfgyfmienircage
SendAddress = config['SMTP']['SendAddress']
AutoTime1 = config['SMTP']['AutoTime1']
AutoTime2 = config['SMTP']['AutoTime2']
#显示在UI界面
window['_Server'].update(Smtp)
window['_Port'].update(Port)
window['_AutoCode'].update(AutoCode)
window['_From'].update(SendAddress)
window['_AutoTime1'].update(AutoTime1)
window['_AutoTime2'].update(AutoTime2)
window['_State'].update('当前状态: 邮件服务器配置文件显示成功!')
except:
window['_State'].update('当前状态: 邮件服务器配置文件显示失败,请确认是否有配置文件!')
sg.popup('配置文件显示失败,请确认是否有配置文件', title='提示', )
def Save_ServerEmail():
"添加邮件服务器的节和配置的项的值"
创建一个ConfigParser对象
config = ConfigParser()
config.add_section('SMTP')
config.set('SMTP', 'Smtp', values['_Server']) # 邮件服务器地址smtp及
config.set('SMTP', 'Port', values['_Port']) # 端口号port
config.set('SMTP', 'SendAddress', values['_From']) # 发件人邮箱地址
config.set('SMTP', 'AutoTime1', values['_AutoTime1']) #定时1
config.set('SMTP', 'AutoTime2', values['_AutoTime2']) #定时1
#保存前先判断是不是新建的配置文件,如果是新建的加密,否则取出配置解密后判断是不是和当前显示的一样
if not os.path.exists(app_path):
config.set('SMTP', 'AutoCode', AES_Encrypt(key, values['_AutoCode'])) # 发件人授权码加密后再保存
添加数据库的节和配置项的值供保存使用
config.add_section('database')
config.set('database', 'ServerName', values['_ServerName'])
config.set('database', 'Database', values['_Database'])
config.set('database', 'LogId', values['_LogId'])
config.set('database', 'LogPassword', AES_Encrypt(key, values['_LogPassword']))
config.set('database', 'LogPassword', values['_LogPassword'])
else:
config.read(app_path, encoding="utf-8") #读取ini配置文件
AES_AutoCode = config['SMTP']['AutoCode']
if AES_AutoCode != values['_AutoCode']:
config.set('SMTP', 'AutoCode', AES_Encrypt(key, values['_AutoCode'])) # 发件人授权码加密后再保存
else:
config.set('SMTP', 'AutoCode', values['_AutoCode']) # 发件人授权码
AES_LogPassword = config['database']['LogPassword']
if AES_LogPassword != values['_LogPassword']:
config.set('database', 'LogPassword', AES_Encrypt(key, values['_LogPassword'])) # 用户名密码加密后再保存
else:
config.set('database', 'LogPassword', values['_LogPassword']) # 用户名密码
# 写入配置文件
with open(app_path, 'w', encoding="utf-8") as configfile:
config.write(configfile)
print('邮件服务器配置文件保存成功')
window['_State'].update('当前状态: 邮件服务器配置文件保存成功!')
sg.popup('邮件服务器配置保存成功', title='提示', )
def Connect_email_server():
"连接邮件服务器前先显示配置信息(不知道为什么要等此函数连接成功才会显示,先显示在界面再取值取不到?)"
#显示在UI界面
show_ini()
global Server
if Server!='':
window['_State'].update('当前状态: 邮件服务器已经连接中,无需重复测试!')
Server.quit()
return Server
创建一个ConfigParser对象
config = ConfigParser()
从 INI文件中读取配置信息
config.read(app_path, encoding="utf-8")
#授权码要解密一下再去连接,不直接明文显示在界面上
AutoCode = AES_Decrypt(key,config['SMTP']['AutoCode'])
#print(AutoCode)
try:
连接服务器
Server = smtplib.SMTP_SSL(config['SMTP']['Smtp'], config['SMTP']['Port'])
登录邮箱
loginResult = Server.login(config['SMTP']['SendAddress'], AutoCode)
print(loginResult)
if loginResult[0]==235:
print('恭喜,邮件服务器连接成功')
window['_State'].update('当前状态: 邮件服务器连接成功')
return(Server)
else:
print('邮件服务器连接失败,请确认配置是否正确后重试')
window['_State'].update('当前状态: 邮件服务器连接失败,请确认配置是否正确后重试')
return
except:
window['_State'].update('当前状态: 邮件服务器连接失败,请确认配置是否正确后重试')
sg.popup('邮件服务器连接失败,请确认配置是否正确后重试', title='提示', )
Server.quit()
return
def Send_file_email(content_data):
"此函数为,执行发送邮件的函数,可带附件"
#先进行数据有效性验证
info = Data_Effect()
if info != 'OK':
return
try:
收发相关信息
SendAddress = values['_From'] # 发件人昵称和地址
ToMail = values['_To'] # 收件人昵称和地址
ToCopy = values['_To_copy'] # 抄送人昵称和地址
Subject = values['_Subject'] # 邮件主题
print(ToMail)
定义一个可以添加正文和附件的邮件消息对象
msg = MIMEMultipart()
收件人相关信息
msg['From'] = SendAddress
msg['To'] = ToMail
msg['Cc'] = ToCopy
msg['Subject'] = Subject
邮件正文,从脚本GetDinData.getdata()中获取
content = content_data
先通过MIMEText将正文规范化,构造成邮件的一部分,再添加到邮件消息对象中
msg.attach(MIMEText(content, 'plain', 'utf-8'))
附件(添加多个附件同理),暂时关闭,有需要再打开
以二进制形式将文件的数据读出,再使用MIMEText进行规范化
attachment = MIMEText(open(f'image/logo.gif', 'rb').read(), 'base64', 'utf-8')
# 告知浏览器或邮件服务器这是字节流,浏览器处理字节流的默认方式为下载
attachment['Content-Type'] = 'application/octet-stream'
# 此部分主要是告知浏览器或邮件服务器这是一个附件,名字叫做xxxxx,这个文件名不要用中文,不同邮箱对中文的对待形式不同
attachment['Content-Disposition'] = 'attachment;filename="logo.gif"'
msg.attach(attachment)
调用函数后返回的服务器对象
Server = Connect_email_server()
向服务器提交邮件发送 ,ToMail 收件人
Server.sendmail(SendAddress, [ToMail], msg.as_string())
print('邮件发送成功')
return 'Success'
except:
return 'Faile'
sg.popup('邮件发送成功', title='提示', )
#发送成功后关闭Server
Server.close()
def Auto_Send(window,AutoFlag):
"数据有效性验证"
info = Data_Effect()
if info !='OK':
return
if AutoFlag:
定时1
Auto_H1 = int(values['_AutoTime1'][:2])
Auto_M1 = int(values['_AutoTime1'][-2:])
target_time1 = time(Auto_H1, Auto_M1,10)
定时2
Auto_H2 = int(values['_AutoTime2'][:2])
Auto_M2 = int(values['_AutoTime2'][-2:])
target_time2 = time(Auto_H2, Auto_M2, 10)
# 无限循环,每次检查当前时间是否达到设定时间
while True:
now = t.localtime()
now = t.strftime("%H:%M:%S",now)
print('等待执行中'+str(now))
if now == str(target_time1) or now == str(target_time2):
# # 如果当前时间达到或超过设定时间,执行以下操作
print("1:预设时间到,开始执行操作")
# 你的代码放这里,连接数据库取值
data = GetDinData.GetData()
# print(data)
window['_Content'].Update(data)
#判断是不是周末或节日
if GetDinData.Getworktype() == "周末" or GetDinData.Getworktype() == "节日":
#自动执行发送邮件结果,返回Success表示成功
if Send_file_email(data) == 'Success':
log_txt ='系统时间:' + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ' 执行自动发邮件成功!'
else:
log_txt ='系统时间:' + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ' 执行自动发邮件失败!'
#执行函数写入日志文件:log.txt
Write_Log(log_txt)
# break
#等待1秒
t.sleep(1)
window['_State'].update('当前状态: 自动发送邮件运行中....')
else:
window['_State'].update('当前状态: 系统己就绪等待执行指令')
# print('关闭自动发送邮件')
def Write_Log(log_text):
写入日志,检查文件是否存在不存在先创建log.txt
if not os.path.exists(log_path):
文件不存在,则创建文件
with open(log_path, 'w') as file:
将字符串写入文件
file.write(log_text)
file.close()
else:
with open(log_path, 'w') as file:
file.write(log_text)
file.close()
六:# -----------服务器数据库配置相关函数----------------------
def Show_Config():
创建一个ConfigParser对象
config = ConfigParser()
if not os.path.exists(app_path):
sg.popup('没有'+app_path+'配置文件,请先在当前界面配置邮件服务器并保存配置文件', title='提示', )
return 'N'
else:
从 INI 文件中读取配置信息
config.read(app_path, encoding="utf-8")
获取指定节点下的键值对
Server = config['database']['ServerName']
DataBase = config['database']['Database']
LogId = config['database']['LogId']
LogPassword = config['database']['LogPassword']
#把配置信息显示在界面上
window['_ServerName'].update(Server)
window['_Database'].update(DataBase)
window['_LogId'].update(LogId)
window['_LogPassword'].update(LogPassword)
window['_State'].update('当前状态: 服务器数据库配置文件显示成功!')
return 'Y'
def Save_ServerDB():
Save_ServerEmail()
创建一个ConfigParser对象
config = ConfigParser()
if ini()'N':
return
else:
# 设置键值对
config.add_section('database')
config.set('database', 'ServerName', values['_ServerName'])
config.set('database', 'Database', values['_Database'])
config.set('database', 'LogId', values['_LogId'])
config.set('database', 'LogPassword', values['_LogPassword'])
# 写入配置文件
with open(app_path, 'w',encoding="utf-8") as configfile:
config.write(configfile)
window['_State'].update('当前状态: 服务器数据库配置文件保存成功!')
sg.popup('服务器数据库配置文件保存成功!', title='提示', )
def connect_db():
"连接前先显示出配置信息在界面方便核对"
if Show_Config()'N':
return
创建一个ConfigParser对象
config = ConfigParser()
从 INI 文件中读取配置信息
config.read(app_path,encoding="utf-8")
获取指定节点下的键值对
Server = config['database']['ServerName']
DataBase = config['database']['Database']
LogId = config['database']['LogId']
LogPassword = config['database']['LogPassword']
LogPassword = AES_Decrypt(key,LogPassword) #显示出解密后的密码
print(LogPassword)
try:
conn = pyodbc.connect('DRIVER={SQL Server};SERVER=' + Server + ';DATABASE=' + DataBase + ';UID=' + LogId + ';PWD=' + LogPassword )
print('当前状态: 服务器数据库连接成功!')
window['_State'].update('当前状态: 服务器数据库连接成功!')
return conn
except Exception as e:
print('当前状态: 数据库连接失败,请检查数据库配置文件!'+str(e))
sg.popup('数据库连接失败,请检查数据库配置文件!', title='提示', )
return 'conn_faild'
七:
事件循环并获取输入值
while True:
event, values = window.read()
打开时自动点击显示服务器邮件配置信息
if event in (None, '_Close'): # 如果用户关闭窗口或点击Close
break
elif event'_SaveConfigEmail':
Save_ServerEmail() #保存邮件服务器配置
elif event == '_ConnectEmail':
Connect_email_server() #连接邮件服务器
elif event'_ShowAutoCode':
show_ini() #显示配置文件
elif event'_Send':
Send_file_email('你好,这是一封测试邮件不用理会!')
elif event'_AutoSend': #开启、关闭自动发送
if not AutoFlag:
window.find_element('_AutoSend').update('关闭自动发送')
window.find_element('_State').update('运行状态: 开启自动发送邮件中......')
AutoFlag = True
#利用多线程,防止程序卡死无响应状态
_thread.start_new_thread(Auto_Send, (window,AutoFlag))
Auto_Send(window,AutoFlag)
else:
window.find_element('_AutoSend').update('开启自动发送')
window.find_element('_State').update('运行状态: 还未开启自动发送邮件')
thread_id = _thread.get_ident()
t.sleep(2)
AutoFlag = False
Auto_Send(window,AutoFlag)
#-------------服务器配置的事件放在下面---------------
elif event'_ShowConfig':
Show_Config()
elif event'_SaveConfigDB':
Save_ServerDB() #保存服务器数据库配置
elif event == '_ConnectDB':
connect_db() # 连接服务器数据库
elif event == '_Get':
data = GetDinData.GetData()
print(data)
window['_Content'].Update(data)
关闭邮件服务器连接和窗口
window.close()
这样配置好了,点开始自动发送邮件如下图:测试时间到自动发送了:
邮箱中看到的邮件:
我用的是QQ邮箱,授权码要自己去申请一下。