Python3实现自动定时发送邮件功能

开篇一张图,内容全靠编…

有时候我们通过爬虫程序爬取了目标信息后需要将这样数据信息打包压缩再自动发送邮件到某个特定邮箱,那么如何实现Python自动发送邮件的功能呢?接下来我们就来简单的介绍下如何利用Python来实现自动发送邮件的功能。
Python SMTP发送邮件

SMTP是发送邮件的协议,Python内置对SMTP的支持,可以发送纯文本邮件HTML邮件以及带附件的邮件。PythonSMTP支持有smtplibemail两个模块,email负责构造邮件,smtplib负责发送邮件。
本着面向对象编程的思想,首先我们封装一个EmailManager类, 这样以后只要通过这个类构造出一个对象再调用send()方法就能轻松实现定时自动发送邮件功能,结构大概如下所示:

class EmailManager:
    def __init__(self, **kwargs):
        ...

    def __get_cfg(self, key, throw=True):
       ...
       
    def __init_cfg(self):
        ...
        
    def login_server(self):
        ...
        
    def get_main_msg(self):
        ...

    def get_attach_file(self):
       ...

    def _format_addr(self, s):
       ...

    def send(self):
        ...

初始化相关配置信息
  • __init__(self, **kwargs)
    类似于Java代码中的构造函数,主要就是进行一些初始化的设置工作,其中参数**kwargs表示的是传入的是一组可变参数,如:
    cfgs={"from":'****', "to":"****", ...}
    这里我们主要是对smtp server服务器、可变参数及附件文件的大小进行一些设定工作。对于smtp服务器如果选择qq邮箱就设置为smtp.qq.com,其他邮箱可以自行google解决,本例使用的即为QQ邮箱,附件的大小我设置为10M,具体的设置请参见代码:
    def __init__(self, **kwargs):
        '''
        constructor
        :param kwargs:Variable paramete
        '''
        self.kwargs = kwargs
        self.smtp_server = 'smtp.qq.com'
        self.MAX_FILE_SIZE = 10 * 1024 * 1024

设置配置参数

接下来我们就需要处理怎么获取外部设置好的配置参数,参数配置采用的是key-value的键值对模式,这样方便配置和获取,下面是一个配置文件的参考例子:

mail_cfgs = {'msg_from': 'xxxx@qq.com',
            'password': 'xxxx',
            'msg_to': ['xxxx@qq.com'],
            'msg_subject': 'Python Auto Send Email Test',
            'msg_content': 'Hi, boy! Just do it, python!',
            'attach_file': r'.\font.zip',
            'msg_date': time.ctime()
            }

其中:

  • msg_from:邮件发送者的地址
  • password:邮件发送者邮箱的密码(这里填的是QQ邮箱的授权码,至于何为授权码,请自行百度或google设置!!)
  • msg_to:邮件接收者的地址,注意这个是个list,因为邮件的接收者可能不止一个
  • msg_subject:邮件的主题
  • msg_content:邮件的主要内容
  • attach_file:邮件的附件名
  • msg_date:邮件的发送时间戳

获取配置参数

接下来我们就来写个方法通过读取key获取这些配置参数,当读取的key对应的valueNone或空值时抛出异常信息,方法代码如下:

    def __get_cfg(self, key, throw=True):
        '''
        get the configuration file based on the key
        :param key:
        :param throw:
        :return:
        '''
        cfg = self.kwargs.get(key)
        if throw == True and (cfg is None or cfg == ''):
            raise Exception("The configuration can't be empty", 'utf-8')
        return cfg

有了读取配置参数的方法就可以将之前配置的mail_cfgs参数读取出来,我们来写个__init_cfg()方法来初始化读取到的这些参数信息,以便接下来的程序可以调用相关配置参数。代码如下:

    def __init_cfg(self):
        self.msg_from = self.__get_cfg('msg_from')
        self.password = self.__get_cfg('password')
        self.msg_to = ';'.join(self.__get_cfg('msg_to'))
        self.msg_subject = self.__get_cfg('msg_subject')
        self.msg_content = self.__get_cfg('msg_content')
        self.msg_date = self.__get_cfg('msg_date')
        # attachment
        self.attach_file = self.__get_cfg('attach_file', throw=False)

登录SMTP服务器

好,这样我们就可以在接下来的代码中自由的调用之前的配置参数了。有小伙伴可能会对代码中的self不甚理解,你可以把它看成是Java代码中的关键字this
所有的配置工作都已经完成了,接下来我们要做的就是登陆smtp服务器,我这里登陆的是QQ邮箱的服务器。可能很多小伙伴在网上看见其他大牛写得登陆邮箱代码是这句:server = smtplib.SMTP(self.smtp_server, 25),然后你屁颠屁颠的运行自己的脚本,结果发现报错,根本就无法运行,你肯定一头雾水!!


其实这是QQ邮箱的问题,它采用的是SMTP_SSL的加密方式,你只需将代码改为server = smtplib.SMTP_SSL(self.smtp_server, 465)即可,注意相应的端口号变成了465。参考代码如下:

    def login_server(self):
        '''
        login server
        :return:
        '''
        server = smtplib.SMTP_SSL(self.smtp_server, 465)
        server.set_debuglevel(1)
        server.login(self.msg_from, self.password)
        return server

这里我们返回这个server是因为后面的代码中需要用到,用set_debuglevel(1)就可以打印出和SMTP服务器交互的所有信息。


处理邮件内容

登陆成功后接下来我们就需要处理邮件的主要内容了。一封正常的邮件一般包含有收发件者信息,邮件主题,邮件正文内容,有些邮件还附带有附件(本例就附带压缩包附件),具体的设置参见如下代码:

    def get_main_msg(self):
        '''
        suject content
        :return:msg
        '''
        msg = MIMEMultipart()
        # message content
        msg.attach(MIMEText(self.msg_content, 'plain', 'utf-8'))

        msg['From'] = self._format_addr('From <%s>' % self.msg_from)
        msg['To'] = self._format_addr('To <%s>' % self.msg_to)
        msg['Subject'] = Header(self.msg_subject, 'utf-8')
        msg['Date'] = self.msg_date

        # attachment content
        attach_file = self.get_attach_file()
        if attach_file is not None:
            msg.attach(attach_file)
        return msg

代码中调用了两个方法:_format_addr()get_attach_file()

  • _format_addr()来格式化一个邮件地址。注意不能简单地传入name addr@example.com,因为如果包含中文,需要通过Header对象进行编码。参考代码如下:
    def _format_addr(self, s):
        name, addr = parseaddr(s)
        return formataddr((Header(name, 'utf-8').encode(), addr))
  • get_attach_file()是用来获取附件的相关信息
    带附件的邮件可以看做包含若干部分的邮件:文本和各个附件本身,所以,可以构造一个MIMEMultipart对象代表邮件本身,然后往里面加上一个MIMEText作为邮件正文,再继续往里面加上表示附件的MIMEBase对象即可,我们把附件那部分抽取到get_attach_file()方法中。代码如下:
    def get_attach_file(self):
        '''
        generate mail attachment content
        :return:
        '''
        if self.attach_file is not None and self.attach_file != '':
            try:
                if getsize(self.attach_file) > self.MAX_FILE_SIZE:
                    raise Exception('The attachment is too large and the upload failed!!')
                with open(self.attach_file, 'rb') as file:
                    ctype, encoding = mimetypes.guess_type(self.attach_file)
                    if ctype is None or encoding is not None:
                        ctype = 'application/octet-stream'
                    maintype, subtype = ctype.split('/', 1)
                    mime = MIMEBase(maintype, subtype)
                    mime.set_payload(file.read())
                    # set header
                    mime.add_header('Content-Disposition', 'attachment',
                                    filename=os.path.basename(self.attach_file))
                    mime.add_header('Content-ID', '<0>')
                    mime.add_header('X-Attachment-Id', '0')
                    # set the attachment encoding rules
                    encoders.encode_base64(mime)
                    return mime
            except Exception as e:
                print('%s......' % e)
                return None
        else:
            return None

这里我们先对附件的大小进行判断,如果超过我们设置的10M上限就抛出异常信息,否则就对附件进行一些必要的参数设置,比如信息头和编码规则等。mime.add_header('Content-Disposition','attachment',filename=os.path.basename(self.attach_file))这里的filename可以重新设置附件的名称。mimetypespython自带的标准库,可以根据文件的后缀名直接得到文件的MIME类型。


发送邮件

接下来我们就再写个发送邮件的方法send(),主要就是将上面所讲的方法串联起来,这样当我们生成一个EmailManager对象后可以直接通过对象调用send()方法进行邮件发送操作。send()方法也很简单,直接上代码:

    def send(self):
        try:
            # initialize the configuration file
            self.__init_cfg()
            # log on to the SMTP server and verify authorization
            server = self.login_server()
            # mail content
            msg = self.get_main_msg()
            # send mail
            server.sendmail(self.msg_from, self.msg_to, msg.as_string())
            server.quit()
            print("Send succeed!!")
        except smtplib.SMTPException:
            print("Error:Can't send this email!!")

这里需要注意的是msg_to,即收件人列表,这是个list,之前的mail_cfgs参数配置信息里特地用的是[],这就表示这里可以放多个收件人地址。有人要问那要是我还抄送邮件给别人怎么办?莫急!只需在配置文件中再配置一个'msg_cc':[xxxx@qq.com,****@qq.com]即可,同时在修改以下方法即可:

    def __init_cfg(self):
        ...
        self.msg_cc = self.__get_cfg('msg_cc')
        ...
    def get_main_msg(self):
        ...
        msg['Cc'] = self._format_addr('To <%s>' % self.msg_cc)
        ...
    def send(self):
        ...
        server.sendmail(self.msg_from, self.msg_to + self.msg_cc, msg.as_string())
        ...

把抄送邮件列表同收件人列表链接起来作为一个参数传进去就OK了。


测试

上面已经将EmailManager这个类的全部方法实现了,接下来我们写个测试程序测试下这个类是否能正常发送邮件。

if __name__ == "__main__":
    mail_cfgs = {'msg_from': 'xxxx@qq.com',
                'password': 'xxxxx',
                'msg_to': ['xxxxx@qq.com'],
                'msg_subject': 'Python Auto Send Email Test',
                'msg_content': 'Hi, boy! Just do it, python!',
                'attach_file': r'.\font.zip',
                'msg_date': time.ctime()
                }

    manager = EmailManager(**mail_cfgs)
    manager.send()

测试结果显示收件人的邮箱能正常接收邮件及附件。
但是我们写这个Python程序的初始目的是让它能自己发送邮件,即无人值守发送,看来我们还得改改这个程序。

  • 设置每5分钟向收件人邮箱发送一封邮件
#第一种方式发送
def  circle_send(manager):
    time_intvl = 5 * 60
    start_time = int(time.time())
    print(start_time)
    while True:
        end_time = int(time.time())
        cost_time = end_time - start_time
        # print(cost_time)
        if cost_time == time_intvl:
            threading.Thread(target=manager.send()).start()
            start_time = end_time
            print('regular send email....%s' % time.ctime(start_time))
        else:
            pass

我们使用多线程来进行异步发送操作:

manager = EmailManager(**mail_cfgs)
circle_send(manager)

注意args=(manager,)中的,不可以去掉,不然程序报错。程序执行后会每5分钟向收件人邮箱发送一封email,只要程序不停止邮件就会不停的发下去!!

  • 每天23:00:00定时发送邮件到收件人邮箱
def regular_send(manager):
    regular_hour = 23
    regular_min = 00
    regular_sec = 00
    while True:
        current_time = time.localtime(time.time())
        # print(current_time.tm_min)
        if (current_time.tm_hour == regular_hour) \
                and (current_time.tm_min == regular_min) \
                and (current_time.tm_sec == regular_sec):
            threading.Thread(target=manager.send()).start()
            print('send a email at 23:00:00 every day....')
        else:
            pass

同样使用多线程来进行异步发送操作:

manager = EmailManager(**mail_cfgs)
circle_send(manager)
  • 上面都是通过硬编码的方式实现定时发送功能,这样做虽然功能实现了但是code太多,实现方式低级!接下来,我们通过使用python任务定时运行库 schedule 模块来实现定时功能,简单方便省事,代码量瞬间缩减大半,是不是so easy?!
#第二种方式,使用python任务定时运行库 schedule 模块
def send_mail_by_schedule(manager):
    schedule.every(5).minutes.do(manager.send())              #每5分钟执行一次
    schedule.every().hour.do(manager.send())                   #每小时执行一次  
    schedule.every().day.at("23:00").do(manager.send())        #每天23:00执行一次  
    schedule.every().monday.do(manager.send())                 #每周星期一执行一次
    schedule.every().wednesday.at("22:15").do(manager.send())  #每周星期三22:15执行一次
   
    while True:
        schedule.run_pending()
        time.sleep(1)

if __name__=="__main__":
    manager = EmailManager(**mail_cfgs)
    send_mail_by_schedule(manager)

这种方式也可以实现定时发送邮件功能,代码量也少,但是假如每5分钟运行100个任务,每个任务耗时2分钟,这样在下一个5分钟到来时,上一轮的任务仍在运行,然后又开始了新一轮的任务。解决这个问题的方法是,为每个任务创建一个线程,让任务在线程中运行,从而并行工作。

def run_threaded(manager):
    threading.Thread(target=manager.send()).start()

def send_mail_by_schedule(manager):
    schedule.every(5).minutes.do(run_threaded, manager)   #每5分钟执行一次
  
    while True:
        schedule.run_pending()
        time.sleep(1)

if __name__=="__main__":
    manager = EmailManager(**mail_cfgs)
    send_mail_by_schedule(manager)
  • 以上两种方式推荐使用pythonschedule库实现定时功能,毕竟我一直提倡Code less, Do more!!的做法!
  • 经测试,上述两种状况收件人邮箱均能正常收取邮件。至此,自动发送邮件的功能就全部讲完了。

    例子中我们的附件压缩包是已经打包好的,但实际大多数情况是我们一边运行爬虫程序抓取信息,一边打包压缩再通过自动发送邮件程序发送邮件到收件人邮箱,所以接下我们就要谈谈Python如何压缩/解压文件夹的问题,这个放到稍后的文章中介绍,敬请关注!!

  • 这几天真是要热死人的节奏啊!你要问我有多热,我只能用下面的图片告诉你!!


    不说了我要去睡觉觉了!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,175评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,674评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,151评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,597评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,505评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,969评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,455评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,118评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,227评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,213评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,214评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,928评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,512评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,616评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,848评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,228评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,772评论 2 339

推荐阅读更多精彩内容

  • Python 面向对象Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对...
    顺毛阅读 4,205评论 4 16
  • Linux多线程同步机制 - 信号量信号量函数定义如下:include int semctl(int sem_i...
    molscar阅读 712评论 0 0
  • 一、相关模块介绍 发送邮件主要用到了smtplib和email两个模块,这里首先就两个模块进行一下简单的介绍: 1...
    LizPL阅读 1,342评论 0 3
  • Django 官方文档 发送email 官网地址:[https://docs.djangoproject.com/...
    学以致用123阅读 2,236评论 0 2
  • 从古代的八百里加急,到现在的电子邮件,邮件的发展见证了上下五千年的发展史,这些当然是废话,只是要说说邮件的重要性。...
    大牧莫邪阅读 1,523评论 0 13