引言
好久没发文章了,主要是这段时间太忙了~;另外菜菜有了一个属于自己的博客小站,CSDN好久没写过文章了,写这篇文章也是因为最近菜菜做的网关老是出现网关中的一些关键配置被莫名的干掉的原因,需要写一个监控小程序监控当服务端执行删除事件的时候及时通过邮件的方式的告知管理员,有了这个需求那么我也就迫不及待的试了一下Python3环境下如何优雅的发送邮件~~。
可能有的小伙伴就会问了,为什么不用短信或者微信进行通知,这样不是很有针对性吗?一般在上班的时候看短信或者微信通知多一点。额~,菜菜也知道这两种方式好一点,但是这两种方式才做起来有点复杂。
首先,使用短信的方式你得去买一个短信服务,菜菜在某云上买了一个短信服务,也不贵,年费套餐5000条/年,168人民币;其实公司内部也有自己的短信服务器,不过我看了下文档,只提供了Java的SDK,额~,去死吧;在某云上的购买的短信服务需要验证一些相关的信息,还是蛮复杂的,需要两天的时间,呸~,两天后在开始做估计菜菜要祭天了~,因此短信方案果断舍弃!!!
再者,就是微信通知了,微信通知的方式确实很丝滑,菜菜试了很多种,值得说到的就是itchat了,还是蛮好用的,但是因为菜菜平时工作的时候是需要在电脑上登录微信客户端的,这两个东西是没法共存的,耽误菜菜上班摸鱼撩妹果断舍弃~,不过有兴趣的可以弄个微信小号搞一下,或者用你当前的手机号多申请几个微信号也是可行的,网上的方法很多这个自行Google哈~
最后,综上所述,选择发送邮件的方式是最实惠,最安全,最方便的~,因为大一点的公司都有自己的邮件服务器,内网部署服务的话发送邮件还是很及时的,最重要二点就是:安全和实惠!
效果图
1、发送HTML格式的邮件且携带图片
这里可能有心的小伙伴看到了,邮件中的小图片没加载出来,这个不是邮件的问题,是我们公司邮件系统所用的图床有点问题,点击小图片就会加载出来原始图片,预览还是没有问题的。
2、发送HTML格式的邮件且携带附件
3、发送HTML邮件
4、发送文字邮件
以上的几种邮件方式应该把日常的邮件需求都包含进去了,有其他需求的可以私聊菜菜,菜菜有时间可以加上去~
详细代码
可能心机的小伙伴会说了:“啊,怎么屁话这么多,直接给我上代码!”
菜菜弱弱的说:额~,好的!
# !/usr/bin/env python
# -*- coding: utf-8 -*-
"""
+-----------------------------------------------------------------------------------------------+
| DateTime | Author | Describe |
+-----------------------------------------------------------------------------------------------+
| 2021/6/13 12:25 | tianxiaoyong@baosihgt.com | Fixed Version1.o beta |
+-----------------------------------------------------------------------------------------------+
"""
import os.path
import smtplib
import base64
import sys
if sys.version_info < (3, ):
import exceptions
exception = exceptions.Exception
else:
exception = Exception
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from email.mime.text import MIMEText
class Error(exception):
def __init__(self, message):
self._message = message
super(Error, self).__init__(message)
def __str__(self):
"""Converts the message to a string."""
return 'NoticeSenderError:' + ': ' + str(self._message)
class NoticeError(Error):
"""
定义 Noticer 异常类
"""
pass
class CallerError(Error):
pass
class Object:
pass
class Config:
# 发送者,按需填写即可。
sender = "gateway-noticer@gmail.com"
# 接受者,不管你是163邮箱、gmail、QQ邮箱或者公司内部的邮箱都可以,按需填写即可。
receiver = ["tianxiaoyong@gmail.com"]
# 发件服务器地址,例如:smtp.gmail.com:578。
smtp_address = "邮件服务器地址"
# 邮件用户,例如:username: tianxiaoyong@gmail.com password: xxxxxxx。
account = {"username": "用户名", "password": "用户密码"}
class ConvertObject:
def __init__(self, base_dict: dict):
self.__base = base_dict
def __getattr__(self, item):
return self.__base.get(item)
class Caller:
def __init__(self, caller: dict):
self.__caller = ConvertObject(caller)
def __call__(self, category: str, *args, **kwargs):
if not hasattr(self.__caller, category):
raise CallerError("%s has no corresponding calling method: %s" % (self.__caller.__class__.__name__, category))
func = getattr(self.__caller, category)
return func(*args, **kwargs)
class Noticer:
"""
Python 发送邮件
支持各种邮箱地址
"""
CATEGORY_HTML = "html"
CATEGORY_TEXT = "plain"
CATEGORY_ONLY_ATTACHMENT = "attachment"
CATEGORY_IMAG_ATTACHMENT = "attachment_and_image"
def __init__(
self, sender: str = Config.sender, receiver: str or list = Config.receiver,
smtp_address: str = Config.smtp_address, account: dict = Config.account
) -> None:
if not isinstance(sender, str):
raise TypeError("@sender must be string, but got a %s" % type(sender))
if not isinstance(receiver, (str, list)):
raise TypeError("@receiver must be a string or list, but got a %s" % type(receiver))
if not isinstance(smtp_address, str):
raise TypeError("@smtp_address must be a string of mail address.")
if not isinstance(account, dict) or not account.get("username", None) or not account.get("password", None):
raise ValueError("@account must be a dict type parameter "
"and have two pairs of key values username and password")
self.__sender = sender
self.__receiver = receiver
self.__smtp_address = smtp_address
self.__account = ConvertObject(account)
caller = dict()
caller.__setitem__(self.CATEGORY_TEXT, self.__process_plain_or_html)
caller.__setitem__(self.CATEGORY_HTML, self.__process_plain_or_html)
caller.__setitem__(self.CATEGORY_ONLY_ATTACHMENT, self.__process_attachment)
caller.__setitem__(self.CATEGORY_IMAG_ATTACHMENT, self.__process_attachment_and_image)
self.__build_message = Caller(caller)
def __connect(self):
"""
连接smtp远程服务器
:return: None
"""
host, port = self.__smtp_address.split(":")
if not host or not port:
raise ValueError("Bad smtp mail address: %s" % self.__smtp_address)
caller = smtplib.SMTP(host, int(port))
caller.ehlo()
caller.starttls()
try:
caller.login(self.__account.username, self.__account.password)
except smtplib.SMTPAuthenticationError as e:
raise NoticeError("Login error: %s" % e)
self.__caller = caller
@property
def sender(self) -> str:
return self.__sender
@sender.setter
def sender(self, sender: str):
self.__sender = sender
@property
def receiver(self) -> str or list:
return self.__receiver
@receiver.setter
def receiver(self, receiver: str or list) -> None:
self.__receiver = receiver
def __send(self, receiver, message):
"""
邮件发送者,负责统一发送邮件
:param receiver: 接受者的邮件地址
:param message: 消息体
:return:
None
"""
if not hasattr(self, "__caller"):
self.__connect()
try:
self.__caller.sendmail(self.__account.username, receiver, message.as_string())
except smtplib.SMTPException as e:
raise NoticeError("Noticer send error: %s" % e)
def __process_plain_or_html(self, subject: str, content: object):
"""
构造 文字或者HTML邮件
:param subject: 邮件标题
:param content: 邮件内容
:return:
MIMEText实例
"""
message = MIMEText(
content.message, "html" if content.type == self.CATEGORY_HTML else "plain", "utf-8"
)
message.__setitem__("From", self.__sender)
message.__setitem__("Subject", Header(subject, "utf-8"))
return message
def __process_attachment(self, subject: str, content: object):
"""
构造 携带附件的邮件Message
:param subject: 邮件标题
:param content: 邮件内容
:return:
None
"""
message = MIMEMultipart()
message.__setitem__("From", self.__sender)
message.__setitem__("Subject", Header(subject, "utf-8"))
# 定义邮件正文信息
message.attach(MIMEText(content.message, content.message_type, "utf-8"))
# 定义邮件附件信息
if len(content.attachment):
for attachment in content.attachment:
if not os.path.exists(attachment):
raise NoticeError("Attachment not exists: %s" % attachment)
_, filename = os.path.split(attachment)
att = MIMEText(open(attachment, 'rb').read(), 'base64', 'utf-8')
att["Content-Type"] = 'application/octet-stream'
att["Content-Disposition"] = 'attachment; filename=%s' % filename
message.attach(att)
return message
def __process_attachment_and_image(self, subject: str, content: object):
message = MIMEMultipart("related")
message.__setitem__("From", self.__sender)
message.__setitem__("Subject", Header(subject, "utf-8"))
message_alternative = MIMEMultipart('alternative')
message.attach(message_alternative)
message_alternative.attach(MIMEText(content.message, 'html', 'utf-8'))
images = content.images
if not isinstance(images, dict):
raise NoticeError("@images must be a dict.")
if not images:
return message
for target, image in images.items():
if not os.path.exists(image):
raise NoticeError("Image not exists: %s" % image)
image = open(image, "rb")
image_message = MIMEImage(image.read())
image.close()
image_message.add_header("Content-ID", "<%s>" % target.strip())
message.attach(image_message)
return message
def __processor(self, category: str, subject: str, content: object or str):
"""
邮件处理者,将所有的邮件请求在这里统一处理。
:param category: 邮件类型
:param subject: 邮件标题
:param content: 邮件内容
:return:
None
"""
message = self.__build_message(category, subject, content)
if isinstance(self.__receiver, list):
for r in self.__receiver:
message.__setitem__("To", r)
self.__send(r, message)
else:
message.__setitem__("To", self.__receiver)
self.__send(self.__receiver, message)
def send_attachment_and_message(
self, subject: str, message: str, attachments: str or list, message_type: str = CATEGORY_TEXT
) -> None:
"""
发送携带附件的文字或HTML邮件
:param subject: 邮件标题
:param message: 邮件内容
:param attachments: 由附件路径组成的附件列表
:param message_type: 消息的类型,是CATEGORY_HTML还是CATEGORY_TEXT
:return:
None
"""
message_obj = Object()
message_obj.message = message
message_obj.message_type = message_type
message_obj.attachment = attachments
self.__processor(self.CATEGORY_ONLY_ATTACHMENT, subject, message_obj)
def send_image_and_message(self, subject: str, message: str, images: dict) -> None:
"""
发送携带图片的HTML邮件
@images参数之所以是字典是因为在HTML中需要一个容器去容纳你提供的图片,而邮件模板类似下面:
<h3>测试携带图片的邮件</h3>
<hr>
<img src="cid:img001">
<img src="cid:img002">
其中,cid后面对应的img001和img002就相当于你的容器的指针,你的@images参数应该为下面这样:
{
"img001": "/data/images/001.jpg",
"img002": "/data/images/002.jpg"
}
:param subject: 邮件标题
:param message: 邮件内容
:param images: 由图片ID和图片所在的路径组成的字典
:return:
None
"""
message_obj = Object()
message_obj.message = message
message_obj.images = images
self.__processor(self.CATEGORY_IMAG_ATTACHMENT, subject, message_obj)
def send_html(self, subject: str, message: str):
"""
发送 html 格式的邮件
:param subject: 邮件标题
:param message: 邮件内容
:return:
None
"""
message_obj = Object()
message_obj.type = self.CATEGORY_HTML
message_obj.message = message
self.__processor(self.CATEGORY_HTML, subject, message_obj)
def send_message(self, subject: str, message: str):
"""
发送纯文本邮件
:param subject: 邮件标题
:param message: 邮件内容
:return: None
"""
message_obj = Object()
message_obj.type = self.CATEGORY_TEXT
message_obj.message = message
self.__processor(self.CATEGORY_TEXT, subject, message_obj)
if __name__ == '__main__':
noticer = Noticer()
# 测试普通邮件
# =====================
# noticer.send_message("测试普通邮件", "这是一封普通邮件")
# 测试HTML邮件
# =====================
# message = (
# """
# <h3>测试HTML邮件</h3>
# <hr>
# <p>这是一封HTML邮件</p>
# """
# )
# noticer.send_html("测试HTML邮件", message)
# 测试携带附件的邮件
# =====================
# message = (
# """
# <h3>测试附件邮件</h3>
# <hr>
# <p>这是一封带有附件的邮件</p>
# """
# )
#
# attachments = [
# r"D:\devCodes\Python\pythonic\auto_notice.py",
# r"D:\devCodes\Python\pythonic\main.py"
# ]
# noticer.send_attachment_and_message("测试附件邮件", message, attachments, noticer.CATEGORY_HTML)
# 测试携带图片的邮件
message = (
"""
<h3>测试携带图片的邮件</h3>
<hr>
<img src="cid:img001">
<img src="cid:img002">
"""
)
images = {
"img001": r"D:\devCodes\Python\pythonic\001.jpg",
"img002": r"D:\devCodes\Python\pythonic\002.jpg"
}
noticer.send_image_and_message("测试携带图片的邮件", message, images)
至此,使用Python发送邮件也就完成了,如果遇到了什么问题,欢迎在下方留言,或者直接给菜菜发邮件(tianxiaoyongcs@gmail.com),另外别忘记给菜菜点赞哈,嘻嘻~