邮件收发简易系统
预备工作
采取了Python3的编程语言,因为邮件收发一般需要搭建服务器较为麻烦,因此采取了用第三方服务器的替代方案,这里用QQ邮箱自提供的功能POP3/SMTP来实现,因此需要进入自己的QQ邮箱账号进行开通
从而获取自己的授权代码并可通过第三方服务器进行对邮件的各种处理,甚至是群发等功能。
SMTP
首先针对SMTP主要是信件的发送协议,基于TCP/IP协议族实现。这里因为Python有现成的SMTP库封装成了几个函数形式的功能故可以直接实现。在我的程序顶部的注释部分是一个关于SMTP类型的一种邮件构造手段,但因为其极其繁琐,为保证安全和唯一标识采取了多种封装和处理手段(如时间编码等)故放弃,转而采用最为简易的初始文件头+内容构造邮件。
本质上SMTP的流行在于其背后是利用一个不断服务的邮件服务器,发送的信息被滞留于服务器中的发送队列排队,最终发送。而这恰好契合了我们电子邮件的特点——非即时性和持久性。
在编写和分析上,实际上我已经想到了一种攻击服务器的方法,很有意思的是搜索后发现早有人这么干过了,那就是SMTP OVERFLOW
通过发大量垃圾邮件可以阻塞服务器,不过似乎这一问题在早期已经解决。
还有一种是跨协议族的攻击,通过诱导浏览器连接SMTP/FTP服务来收集登录cookie的方法,但这似乎并非主流黑客攻击手段。
在Python的SMTPlib中,主要函数为(端口465或587是SSL标准端口,默认25)
server = smtplib.SMTP_SSL("smtp.qq.com", 465)
用于连接服务器,这里是用的qq服务,且需要登录:
server.login(sender, password)
而后则是核心邮件发送函数,由socket作为基础函数:
server.sendmail(sender, [user, ], msg.as_string())
并最后退出程序:
server.quit()
因为发送有可能失败,我写了一个try exception用于处理异常,并通过修改toret来返回参数表示发送是否成功。
值得一提的是构造邮件规则,我们需要三个文件头:
msg['From'] = formataddr([name, sender]) # 发件人邮箱账号
msg['To'] = formataddr([username, user]) # 收件人邮箱账号
msg['Subject'] = title # 邮件的主题
以及内容的UTF8变长字节编码,这是互联网主流:
msg = MIMEText(text, 'plain', 'utf-8') # 内容编码
这些是最基础的邮件构成,因为SMTP协议需要解析域名DNS地址,发送到服务器,一个路牌和发件人是必不可少的。
不过实际上像subject这种就相对偏个性化一些,在代码的注释部分也可以看到有些邮件是可以没有题目的,因为我们可以把它编入内容作解析。
POP3&IMAP
IMAP与POP3的利弊权衡后选择了POP3,这不是因为我的懒惰(虽然POP3很好写),而是QQ支持的POP3服务已经开启了,我不想换另一个协议族。
但实际上从比较角度,POP3也确实更直观易懂,功能强大:
方法 | 描述 |
---|---|
POP3(server) | 实例化POP3对象,server是pop服务器地址 |
user(username) | 发送用户名到服务器,等待服务器返回信息 |
pass_(password) | 密码 |
stat() | 返回邮箱的状态,返回2元祖(消息的数量,消息的总字节) |
list([msgnum]) | stat()的扩展,返回一个3元祖(返回信息, 消息列表, 消息的大小),如果指定msgnum,就只返回指定消息的数据 |
retr(msgnum) | 获取详细msgnum,设置为已读,返回3元组(返回信息, 消息msgnum的所以内容, 消息的字节数),如果指定msgnum,就只返回指定消息的数据 |
dele(msgnum) | 将指定消息标记为删除 |
quit() | 登出,保存修改,解锁邮箱,结束连接,退出 |
这是IMAP的:
方法 | 描述 |
---|---|
IMAP4(server) | 与IMAP服务器建立连接 |
login(user, pass) | 用户密码登录 |
list() | 查看所有的文件夹(IMAP可以支持创建文件夹) |
select() | 选择文件夹默认是"INBOX" |
search() | 三个参数,第一的是CHARSET,通常为None(ASCII) |
为了查看邮件,我又写了一个recvmail函数用于处理
与SMTP类似,第一步连接服务器:
server = poplib.POP3("pop.qq.com")
和IMAP不一样,因为没有login函数直接用的两个封装函数进行登录:
server.user(sender)
server.pass_(password)
我们可以利用stat()打印邮件总体情况:
print('您收到的邮件数: %s. 空间占用: %s B' % server.stat())
当然这并不够,还需要显示所有的邮件内容,因此我们要遍历邮件列表:
resp, mails, octets = server.list()
for i in range(len(mails), 0, -1): # 打印所有邮件
print("邮件 " + str(len(mails) - i + 1) + ":") # 从最新的开始打印并显示索引
resp, lines, octets = server.retr(i) # 获取最新邮件信息
msg_content = b'\r\n'.join(lines).decode('utf-8') # 获得整个邮件的原始文本
msg = Parser().parsestr(msg_content) # 解析
print_info(msg) # 解码并打印
print("\n\n\n")
这里我们从邮件内容中剥得mail列表,并遍历显示
为此需要先按照UTF8解码
但是,我们也不排斥其它编码格式,毕竟这是难以避免的,因此额外需要一个用于找字符编码格式的函数:
def guess_charset(msg):
charset = msg.get_charset() # 直接用get_charset()方法获取编码
if charset is None: # 如果获取不到,则在原始文本中寻找
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=') # 找'charset='这个字符串
if pos >= 0: # 如果有,则获取该字符串后面的编码信息
charset = content_type[pos + 8:].strip()
return charset
打印过程也比较繁琐浪费时间,主要就是解析三个:来自谁,发给谁,标题,此外必要还处理下内容和附件:
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '')
if value:
if header == 'Subject': # 解码主题信息
value = decode_str(value)
else: # 解码发件人和收件人信息
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = u'%s <%s>' % (name, addr) # 默认utf-8
print('%s%s: %s' % (' ' * indent, header, value))
content_type = msg.get_content_type() # 获取邮件对象格式
if content_type == 'text/plain' or content_type == 'text/html': # 若为文本邮件,则直接打印
content = msg.get_payload(decode=True)
charset = guess_charset(msg) # 检测编码类型
if charset:
content = content.decode(charset) # 解码
print('%sContent:\n %s' % (' ' * indent, content)) # 打印内容
else:
print('%sAttachment: %s' % (' ' * indent, content_type)) # 否则为附件,获取附件信息
至此解析工作就基本做完了,因为我们不一定能成功打印,可能遇到如断网、服务器拒绝等情况,因此还需要加一个try exception如法炮制。并且在完成一切后断开服务器:
server.quit()
演示
采用python3.7解释器,建议用cmd访问程序,并且别忘了跟三个参数,分别是本人的邮箱地址,申请服务的SMTP/POP3服务验证码(密码),以及你喜欢的个人昵称
进入后即可按照指示进行操作,例如展示查看,发送邮件,收邮件
优缺点分析
这个程序首先肯定是不能算真正的邮件系统的,因为连服务器都是找腾讯借的。
如果自己架服务器,自然也是比较麻烦和繁琐的,因为为此还需要一个数据库存放用户信息。
当然,本程序没有做前端也是一个美中不足,但考虑到本人的美工之差和界面修改信号处理的麻烦没有额外处理,但实际上相对于chatroom是界面实现是更简单的,因为这里只有单一线程。
没有为程序做插入附件的接口和下载附件的接口也是一个小瑕疵,如果可以也应当制作这个接口,不过这自然是适合和界面一起实现。
优点上可能更多是强鲁棒性和代码的简易可读性。
参考文献
Python 网络编程第三版,[美]Brandon Rhodes,John Goerzen 第十二、十三、十四章
菜鸟编程SMTP协议
源码:
import smtplib # 邮件发送
import poplib # 邮件收取
import sys # 系统参数
from email.mime.text import MIMEText # 邮件构造
from email.parser import Parser # 文件解析
from email.header import decode_header # 头文件名解析
from email.utils import formataddr, parseaddr # 地址等解析
def main():
if len(sys.argv) < 4: # 若参数少于4个,则告知用法并退出
name = sys.argv[0]
print("使用错误!程序 {} 需要两个参数——你的qq邮箱账号,你的第三方验证码,你的昵称!".format(name))
sys.exit(2)
else:
my_sender = str(sys.argv[1])
my_pass = str(sys.argv[2])
name = str(sys.argv[3])
while True: # 建立循环处理用户的需求
choice = input("请问你想发邮件还是查看邮件?发送0,查看1,退出2\n")
choice = int(choice)
if choice == 0: # 发送邮件
user = input("请输入你的发送对象邮箱地址\n")
user = str(user)
username = input("请输入你的发送对象昵称\n")
username = str(username)
title = input("请输入邮件题目\n")
title = str(title)
txt = input("请输入邮件内容\n")
txt = str(txt)
print("正在发送......")
ret = mail(my_sender, user, my_pass, title, txt, name, username)
if ret:
print("邮件发送成功!\n")
else:
print("邮件发送失败...请重试\n")
elif choice == 1:
ret = recvmail(my_sender, my_pass)
if ret:
print("")
else:
print("无法连接服务器,请检查网络或账号!\n")
else:
break
# 邮件发送函数
def mail(sender, user, password, title, text, name, username):
toret = True
try:
msg = MIMEText(text, 'plain', 'utf-8') # 内容编码
msg['From'] = formataddr([name, sender]) # 发件人邮箱账号
msg['To'] = formataddr([username, user]) # 收件人邮箱账号
msg['Subject'] = title # 邮件的主题
server = smtplib.SMTP_SSL("smtp.qq.com", 465) # 连接发件人邮箱中的SMTP服务器,端口465,默认为25
server.login(sender, password) # 登录邮箱
server.sendmail(sender, [user, ], msg.as_string()) # 发送邮件
server.quit() # 关闭连接
except Exception:
toret = False
return toret
# 邮件收取函数
def recvmail(sender, password):
toret = True
try:
server = poplib.POP3("pop.qq.com") # 连接到服务器
server.user(sender)
server.pass_(password)
print('您收到的邮件数: %s. 空间占用: %s B' % server.stat()) # 显示总情况
resp, mails, octets = server.list() # 每个邮件的具体情况
# print(mails) # [b'1 8539', b'2 1339', b'3 1224', b'4 1268']
for i in range(len(mails), 0, -1): # 打印所有邮件
print("邮件 " + str(len(mails) - i + 1) + ":") # 从最新的开始打印并显示索引
resp, lines, octets = server.retr(i) # 获取最新邮件信息
msg_content = b'\r\n'.join(lines).decode('utf-8') # 获得整个邮件的原始文本
msg = Parser().parsestr(msg_content) # 解析
print_info(msg) # 解码并打印
print("\n\n\n")
server.quit() # 关闭连接
except Exception:
toret = False
return toret
def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset) # 如果文本中存在编码信息,则进行相应的解码
return value
def guess_charset(msg):
charset = msg.get_charset() # 直接用get_charset()方法获取编码
if charset is None: # 如果获取不到,则在原始文本中寻找
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=') # 找'charset='这个字符串
if pos >= 0: # 如果有,则获取该字符串后面的编码信息
charset = content_type[pos + 8:].strip()
return charset
def print_info(msg, indent=0): # indent用于缩进
# 首先打印邮件的发件人,收件人和主题
if indent == 0:
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '')
if value:
if header == 'Subject': # 解码主题信息
value = decode_str(value)
else: # 解码发件人和收件人信息
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = u'%s <%s>' % (name, addr) # 默认utf-8
print('%s%s: %s' % (' ' * indent, header, value))
# 将邮件组合分离,
if (msg.is_multipart()):
parts = msg.get_payload() # 拿取msg的子对象
for n, part in enumerate(parts):
print('%spart %s' % (' ' * indent, n))
print('%s--------------------' % (' ' * indent))
print_info(part, indent + 1)
# 逐一打印邮件对象
else:
content_type = msg.get_content_type() # 获取邮件对象格式
if content_type == 'text/plain' or content_type == 'text/html': # 若为文本邮件,则直接打印
content = msg.get_payload(decode=True)
charset = guess_charset(msg) # 检测编码类型
if charset:
content = content.decode(charset) # 解码
print('%sContent:\n %s' % (' ' * indent, content)) # 打印内容
else:
print('%sAttachment: %s' % (' ' * indent, content_type)) # 否则为附件,获取附件信息
if __name__ == '__main__':
main()
如果喜欢就点个赞吧