Python笔记11(网络编程、电子邮件)

16. 网络编程

网络通信是两台计算机上的两个进程之间的通信,网络编程就是如何在程序中实现两台计算机的通信。

1. TCP/IP简介

互联网协议包含了上百种协议标准,最重要的两个协议是TCP和IP协议,因此,大家把互联网的协议简称TCP/IP协议。

互联网上每个计算机的唯一标识是IP地址,如果一台计算机同时接入两个或更多的网络,比如路由器,它就有两个或多个IP地址,所以,IP地址对应的实际上是计算机的网络接口,通常是网卡

IP协议: 把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,但不保证能到达,也不保证顺序到达。

IP地址: 实际上是一个32位整数(简称IPV4),以字符串表示的IP地址如192.168.0.1实际是把32位整数按8位分组后的数字表示,目的是便于阅读。

IPV6地址: 实际上是一个128位整数,是目前使用的IPV4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334

TCP协议是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。

TCP协议会通过 握手 建立连接,然后对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。

浏览器的HTTP协议、发送邮件的SMTP协议等这些更高级的协议都是建立在TCP协议基础上的。

一个TCP报文包含

  • 要传输的数据
  • 源IP地址和目标IP地址
  • 源端口和目标端口

端口

  • 作用:一个TCP报文传输到计算机时,端口区分该报文传给哪个具体的进程。两个进程在两台计算机之间建立网络连接需要各自的IP地址和各自的端口号
  • 一些公共端口:(小于1024的是Internet标准服务的端口,端口号大于1024的可任意使用)
    • Web服务的标准端口:80
    • SMTP服务:25
    • FTP服务:21
    • MySQL端口:3306
    • Oracle端口:1521

一个进程也可能同时和多个计算机建立链接,因此它会申请很多端口。

2. TCP编程

Socket是网络编程的一个抽象概念。通常用一个Socket表示"打开了一个网络连接",而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。

大多数链接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。

1. 客户端

客户端要主动发起TCP连接,必须知道服务器的IP地址和端口号。

客户端与服务器基于TCP的socket交互步骤

  • 导入socket库:

    import socket
    
  • 创建一个socket

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
  • 与某服务器建立连接(以连接百度服务器为例)。 connect(tuple)

    s.connect(('www.baidu.com', 80))  # connect() 接收一个tuple,指定地址和端口号
    
  • 发送数据。 send()

    s.send(b'GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n')
    
  • 接收数据。 recv(n)n指定接收的最大字节数。由于接收的数据可能过大,一次性读取耗费内存,因此分段接收

    buffer = []
    while True:
    	d = s.recv(1024)
    	if d:
    		buffer.append(d)
    	else:
    		break
    data = b''.join(buffer)
    
  • 接收数据代码解释:

    • 创建保存数据的容器buffer
    • while True是为了一直按每次最多1024字节接收数据,直到没有数据(d为空)后break
    • 利用字符串join()方法,将buffer的内容赋给data
  • 关闭连接

    s.close()
    
  • 处理接收到的数据(接收到的数据包含HTTP头和网页本身)

    header, html = data.split(b'\r\n\r\n', 1)  # 对data切割一次
    
  • 打印header,注意编码格式

    h = header.decode('utf-8')
    print(h)
    
  • 将html写入文件

    with open('baidu.html', 'wb') as f:
    	f.write(html)
    
  • 这样就在当前目录下生成一个baidu.html文件,通过浏览器就打开百度首页

示例:创建一个基于TCP连接的Socket

# 导入socket库
import socket

# 创建一个socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 与百度服务器建立连接(connect()的参数是一个`tuple`,包含地址和端口号)
s.connect(('www.baidu.com', 80))

# 发送数据
s.send(b'GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n')

# 接收数据
buffer = []
while True:
    # 每次最多接收1k字节
    d = s.recv(1024)
    if d:
        buffer.append(d)
    else:
        break
data = b''.join(buffer)

# 关闭连接
s.close()

# data数据包含HTTP头和网页本身,对data进行切割,打印header、保存html的信息到文件
header, html = data.split(b'\r\n\r\n', 1)
# 打印header信息
print(header.decode('utf-8'))
# 把接收的数据写入文件
with open('baidu.html', 'wb') as f:
    f.write(html)
  • 说明:
    • 创建Socket时,AF_INET指定IPV4协议,如果要用更先进的IPV6,就指定为AF_INET6
    • SOCK_STREAM指定使用面向流的TCP协议,这样一个Socket对象就创建成功,但还没建立连接
    • TCP连接创建的是双向通道,双方都可以同时给对方发送数据。但谁先发谁后发、怎么协调,要根据具体的协议来决定
      • 比如:HTTP协议规定客户端必须先发送请求给服务器,服务器收到后才发送数据给客户端
    • 发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收百度服务器返回的数据
    • 接收数据时,调用recv(max)方法,一次最多接收指定的字节数。因此在一个while循环中反复接收,直到recv()返回空数据,表示接收完毕,退出循环
    • 接收完数据后,调用close()方法关闭Socket,这样一次完整的网络通信就结束了
    • 收到的数据包括HTTP头和网页本身,对其进行切割,然后网页内容写入到文件,在浏览器中打开baidu.html文件即可看到百度首页

2. 服务端

服务端编程要比客户端复杂一些。

服务端编程步骤

  • 服务端进程绑定一个固定端口(比如80)监听来自其他客户端的连接
  • 客户端连接过来,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接
  • 服务器会有大量来自客户端的连接,所以服务器要能够区分一个Socket连接是和哪个客户端绑定的
    • 一个Socket依赖4项来唯一确定一个Socket:
      • 服务器地址
      • 服务器端口
      • 客户端地址
      • 客户端端口
  • 服务器还需要同时响应多个客户端的请求,所以每个连接都需要一个新的进程或者新的线程来处理。否则服务器一次就只能服务一个客户端了

步骤

  • testServer.py

  • 首先创建一个基于IPV4和TCP协议的Socket

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
  • 绑定监听的地址和端口。服务器可能有多块网卡,可绑定到某一块网卡的IP地址上;也可用0.0.0.0绑定到所有的网络地址;还可用127.0.0.1绑定到本机地址,127.0.0.1是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,外部计算机无法连接进来。端口号需预先指定。自定义的非标准服务,大于1024的随便指定

    s.bind(('127.0.0.1', 9999))  # bind()接收一个tuple,包含地址和端口号
    
  • 调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量

    s.listen(5)
    
  • 每个服务器的请求连接都必须创建新线程(或进程)来处理,否则单线程在处理连接的过程中,无法接受其他客户端的连接

    def tcplink(sock, addr):
    	sock.send(b'Welcome!')
    	while True:
    		data = sock.recv(1024)
    		time.sleep(1)
    		if not data or data.decode('utf-8') == 'exit':
    			break
    		sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
    	sock.close()
    	print('Connection from %s:%s closed.' % addr)
    
  • 服务器通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接

    while True:
    	# 接受一个新连接
    	sock, addr = s.accept()
    	# 创建新线程来处理TCP连接
    	t = threading.Thread(target=tcplink, args=(sock, addr))
    	t.start()
    
  • 这样建立连接后,服务器首先发一条欢迎消息,然后等待客户端数据,并加上Hello在发送给客户端;若客户端发送了exit字符串,就直接关闭连接

  • 编写一个客户端程序用来测试 testClient.py

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 建立连接:
    s.connect(('127.0.0.1', 9999))
    # 接收欢迎消息:
    print(s.recv(1024).decode('utf-8'))
    for data in [b'Michael', b'Tracy', b'Sarah']:
        # 发送数据:
        s.send(data)
        print(s.recv(1024).decode('utf-8'))
    s.send(b'exit')
    s.close()
    
  • 然后打开两个命令行窗口,先启动服务端,再启动客户端,即可看到效果。

  • 需要注意的是:客户端程序运行完毕就退出了,服务器程序会永远运行下去,必须按Ctrl+c退出程序

示例:编写一个简单的服务器程序,它接收客户端连接,把客户端发过来的字符串加上Hello再发回去。代码如下:

import socket
import threading
import time

# 创建一个基于TIPV4和CP协议的Socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定监听的地址和端口
s.bind(('127.0.0.1', 9999))

# 调用listen()方法开始监听端口
s.listen(5)
print('Waiting for connection...')

# 连接创建新线程(或进程)来处理
def tcplink(sock, addr):
    print('Accept new connection from %s:%s...' % addr)
    sock.send(b'Welcome!')
    while True:
        data = sock.recv(1024)
        time.sleep(1)
        if not data or data.decode('utf-8') == 'exit':
            break
        sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
    sock.close()
    print('Connection from %s:%s closed.' % addr)


# 服务器通过一个永久循环来接受来自客户端的连接
while True:
    # 接受一个新连接
    sock, addr = s.accept()
    # 创建新线程来处理TCP连接
    t = threading.Thread(target=tcplink, args=(sock, addr))
    t.start()
  • 测试结果:

server-client1
两个客户端测试结果

server-client2

小结

1. 用TCP协议进行Socket编程在Python中十分简单

  • 对于客户端,要主动连接服务器的IP和指定端口
  • 对于服务器,首先监听指定端口,然后对每个新的连接,创建一个线程或进程来处理。通常,服务器程序会无限运行下去

2. 注意:同一个端口,被一个Socket绑定之后,就不能被别的Socket绑定了。

3. UDP编程

TCP是建立可靠连接,并且通信双方都可以以 的形式发送数据。相对TCP,UDP是面向无连接的协议

使用UDP协议时,不需要建立连接,只要知道对方的IP地址和端口号,就可以直接发数据包,但能不能到达就不知道了

服务器绑定UDP端口和TCP端口互不冲突,可指定相同的端口。

虽然UDP数据传输不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议

使用UDP协议传输数据,其通信双方也分为客户端和服务器。

步骤:

  • testServer.py

  • 创建socket对象,使用SOCK_DGRAM来指定UDP协议

    s = socket.socket(socket.AF_INET, socket.DGRAM)
    
  • 服务器首先需要绑定端口:

    s.bind(('127.0.0.1', 9999))
    
  • UDP不需要listen()方法,直接接收来自任何客户端的数据

    while True:
    	# 接收数据  recvfrom()方法返回数据和客户端的地址与端口
    	data, addr = s.recvfrom(1024)
    	print('Received from %s:%s.' addr)
    	# sendto()方法把数据用UDP发给客户端
    	s.sendto(b'Hello, %s!' % data, addr')
    
  • testClient.py

  • 客户端使用UDP时,先创建基于UDP的Socket,然后不需要调用connect(),直接通过sendto()给服务器发数据

    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    for data in [b'Jason', b'Bob', b'Alice']:
    	# 发送数据  sendto(data, (ip, port))
    	s.sendto(data, ('127.0.0.1', 9999))
    	# 接收数据  recv(max)
    	print(s.recv(1024).decode('utf-8'))
    s.close()
    

代码如下:

  • 服务端 testServer.py
import socket

# 创建连接对象,SOCK_DGRAM 指定该socket的类型是UDP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 首先绑定端口
s.bind(('127.0.0.1', 9999))

# 不需要调用listen()方法,直接接收来自任何客户端的数据
print('Bind UDP on 9999...')
while True:
    # 接收数据,recvfrom()方法返回数据和客户端的地址和端口
    data, addr = s.recvfrom(1024)
    print('Received from %s:%s.' % addr)
    s.sendto(b'Hello, %s!' % data, addr)

  • 客户端 testClient.py
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

for data in [b'Jason', b'Bob', b'Alice']:
    # 发送数据
    s.sendto(data, ('127.0.0.1', 9999))
    # 接收数据
    print(s.recv(1024).decode('utf-8'))

s.close()
  • 测试结果:

UDP

17. 电子邮件

电子邮件软件(比如OutlookFoxmail等)被称为MUA:Mail User Agent——邮件用户代理。

电子邮件发送流程:我网易邮箱me.163.com → 朋友新浪邮箱friend@sina.com

  • 我编辑邮件,使用MUA发出,到达网易的MTA:Mail Transfer Agent——邮件传输代理(Email服务提供商,如网易、新浪等)
  • 网易的MTA把邮件发到新浪的服务商(即新浪的MTA)。该过程可能还会经过别的MTA,但无需关心,只关心速度
  • 新浪的MTA把邮件投递到邮件的最终目的地 MDA:Mail Delivery Agent——邮件投递代理。Email到达MDA后就存放在新浪的某个服务器的某个文件或特殊的数据库里,这个长期保存邮件的地方称之为电子邮箱
  • 朋友通过MUA从MDA上把邮件取到自己的电脑上

总结来说:发件人 → MUA → MTA → MTA → 若干个MTA → MDA ← MUA ← 收件人

编写程序来发送和接收邮件,本质上是:

  • 编写MUA把邮件发到MTA
  • 编写MUA从MDA上收邮件

发邮件时,MUA和MTA使用的协议就是SMTP:Simple Mail Transfer Protocol,后面的MTA到另一个MTA也是用SMTP协议。

收邮件时,MUA和MDA使用的协议有两种POP:Post Office Protocol;IMAP:Internet Message Access Protocol。

邮件客户端软件在发邮件时,先配置SMTP服务器,即要发到哪个MTA上,假设使用的163邮箱,就不能直接发到新浪的MTA上,因为它只服务于新浪的用户。因此需要填163提供的SMTP服务器地址:smtp.163.com,还需要填写你的邮箱地址和口令,这样163的MUA才能正常把Email通过SMTP协议发送到MTA。收邮件时也类似。

1. SMTP发送邮件

1. 使用SMTP发送邮件

SMTP是发送邮件的协议,Python内置对SMTP的支持,可发送 纯文本邮件、HTML邮件以及带附件的邮件

Python对SMTP支持有smtplibemail两个模块,email负责构造邮件,smtplib负责发送邮件。

发送邮件步骤:( 可忽略,看下边的详细邮件步骤

  • 导入相应模块

    from email.mime.text import MIMEText
    
  • 构造一个最简单的纯文本邮件

    • MIMEText对象:第一个参数是邮件正文
    • 第二个参数是MIME的subtype,传入'plain'表示纯文本
    • 最终的MIME就是'text/plain'
    • 最后一定要用utf-8编码保证多语言兼容性
    msg = MIMEText('Hello, send by Python...', 'plain', 'utf-8')
    
  • 通过SMTP发出去

    • set_debuglevel(1)打印和SMTP服务器交互的所有信息
    • SMTP协议就是简单的文本命令和响应
    • login()方法用来登录SMTP服务器
    • sendmail()方法发邮件。因为可以一次发给多个人,所以传入一个list,邮件正文是一个str
    • as_string()MIMRText对象变成str
    # 输入发件人Email地址和口令
    from_addr = input('From: ')
    password = input('Password: ')
    # 输入收件人地址
    to_addr = input('To: ')
    # 输入SMTP服务器地址 网易:smtp.163.com  QQ:smtp.qq.com
    smtp_server = input('SMTP server: ')  # 该SMTP是发件人邮箱的SMTP服务器地址
    
    import smtplib
    
    # SMTP协议默认端口是25
    server = smtplib.SMTP(smtp_server, 25)
    # 打印出和SMTP服务器交互的所有信息
    server.set_debuglevel(1)
    # 登录SMTP服务器
    server.login(from_addr, password)
    # 发邮件 sendmail()
    server.sendmail(from_addr, [to_addr], msg.as_string())
    # 退出
    server.quit()
    

代码如下:

from email.mime.text import MIMEText

# 构造一个最简单的纯文本邮件
msg = MIMEText('Hello, send by Python...', 'plain', 'utf-8')

# 通过SMTP发出去
# 输入Email地址和口令
# from_addr = input('From: ')
from_addr = '577155188@qq.com'
# password = input('Password: ')
password = 'zsaivahrhmipbccg'
# 输入收件人地址
# to_addr = input('To: ')
to_addr = '13436603789@163.com'
# 输入SMTP服务器地址
# smtp_server = input('SMTP server: ')  # smtp.163.com 或 smtp.qq.com
smtp_server = 'smtp.qq.com'

import smtplib

# SMTP协议默认端口是25
server = smtplib.SMTP(smtp_server, 25)
# 打印出和SMTP服务器交互的所有信息
server.set_debuglevel(1)
# 登录SMTP服务器
server.login(from_addr, password)
# 发邮件 sendmail()
server.sendmail(from_addr, [to_addr], msg.as_string())
# 退出
server.quit()

执行输入相关信息后,会打印出详细的与服务器交互的信息,并且在收件人邮箱里会有该邮件:

qq_to_163
仔细观察,发现如下问题:

  • 邮件没有主题
  • 收件人的名字没有显示为友好的名字,收件人是 (无)
  • 明明收到了邮件,却提示不在收件人中

这是因为邮件主题、如何显示发件人、收件人等信息并不通过SMTP协议发给MTA,而是包含在发给MTA的文本中。因此,需要把FromTosubject添加到MIMEText中,才是一封完整的邮件:

发送完整邮件的步骤

  • 编写格式化一个邮件地址的函数。不能简单的传入name <addr@example.com>,因为若包含中文,需要通过Header对象进行编码

    from email.header import Header
    from email.mime.text import MIMEText
    from email.utils import parseaddr, formataddr
    import smtplib
    
    def _format_addr(s):
        name, addr = parseaddr(s)
        return formataddr((Header(name, 'utf-8').encode(), addr))
    
  • 获取输入的相关信息:发件人邮箱和口令、收件人邮箱地址、SMTP服务器地址

    from_addr = input('From: ')
    password = input('Password: ')
    to_addr = input('To: ')
    smtp_server = input('SMTP server: ')
    
  • 编写msg的相关信息,msg是一个email的class

    msg = MIMEText('Hello, send by Python...', 'plain', 'utf-8')
    msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)
    # `msg['To']`接收的是字符串而不是list,如果有多个邮件地址,用`,`分隔即可
    msg['To'] = _format_addr('管理员 <%s>' % to_addr)
    msg['Subject'] = Header('来自SMTP的问候...', 'utf-8').encode()
    
  • 编写SMTP发送设置

    server = smtplib.SMTP(smtp_server, 25)
    server.set_debuglevel(1)  # 打印与服务器交互的信息可不写
    server.login(from_addr, password)
    server.sendmail(from_addr, [to_addr], msg.as_string())
    server.quit()
    

具体代码:

from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr

import smtplib


# 编写格式化一个邮件地址的函数。不能简单的传入`name <addr@example.com>`,因为若包含中文,需要通过`Header`对象进行编码
def _format_addr(s):
    name, addr = parseaddr(s)
    return formataddr((Header(name, 'utf-8').encode(), addr))


# 获取输入的相关信息:发件人邮箱和口令、收件人邮箱地址、SMTP服务器地址
from_addr = input('From: ')
password = input('Password: ')
to_addr = input('To: ')
smtp_server = input('SMTP server: ')

msg = MIMEText('Hello, send by Python...', 'plain', 'utf-8')
msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)
msg['To'] = _format_addr('管理员 <%s>' % to_addr)
msg['Subject'] = Header('来自SMTP的问候...', 'utf-8').encode()

server = smtplib.SMTP(smtp_server, 25)
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()

执行代码,输入相关信息,成功后会在163的邮箱里看到该邮件:
2

2. 发送HTML邮件

发送HTML邮件(不是普通的纯文本文件)的方法:在构造MIMEText对象时,把HTML字符串传进去,再把第二个参数由plain变为html就可以了。

msg = MIMEText('<html><body><h1>Hello</h1>' +
               '<p>send by <a href="http://www.python.org">Python</a>...</p>' +
               '</body></html>', 'html', 'utf-8')

其他不变。再发送一遍邮件,你将看到以HTML显示的邮件:

在这里插入图片描述

  • 点击 Python的话,会转向Python的官网。

3. 发送附件

Email中加入附件:构造一个MIMEMultipart对象代表邮件本身,然后往里面加一个MIMEText作为邮件正文,再继续往里面加表示附件的MIMEBase对象即可。

代码如下:

from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import smtplib

# 编写格式化一个邮件地址的函数。不能简单的传入`name <addr@example.com>`,因为若包含中文,需要通过`Header`对象进行编码
def _format_addr(s):
    name, addr = parseaddr(s)
    return formataddr((Header(name, 'utf-8').encode(), addr))

# 获取输入的相关信息:发件人邮箱和口令、收件人邮箱地址、SMTP服务器地址
from_addr = input('From: ')
password = input('Password: ')
to_addr = input('To: ')
smtp_server = input('SMTP server: ')

# 邮件对象
msg = MIMEMultipart()
msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)
msg['To'] = _format_addr('管理员 <%s>' % to_addr)
msg['Subject'] = Header('来自SMTP的问候...', 'utf-8').encode()

# 邮件正文是MIMEText
msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))

# 添加附件就是加上一个MIMEBase,从本地读取一个图片
with open('image.png', 'rb') as f:
    # 设置附件的MIME和文件名,这里是png类型
    mime = MIMEBase('image', 'png', filename='image.png')
    # 加上必要的头信息
    mime.add_header('Content-Disposition', 'attachment', filename='test.png')
    mime.add_header('Content-ID', '<0>')
    mime.add_header('X-Attachment-Id', '0')
    # 把附件的内容读进来
    mime.set_payload(f.read())
    # 用Base64编码
    encoders.encode_base64(mime)
    # 添加到MIMEMultipart
    msg.attach(mime)

# 发送邮件
server = smtplib.SMTP(smtp_server, 25)
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()

然后,按正常发送流程把msg(注意类型已变为MIMEMultipart)发送出去,就可以收到如下带附件的邮件:
fujian

4. 发送图片

把一个图片嵌入到邮件正文中:按照发送附件的方式,先把邮件作为附件添加进去,然后在HTML中通过引用src="cid:0"就可以把附件作为图片嵌入。如果有多个图片,就给它们依次编号,引用不同的cid:x即可。

  • 直接在HTML邮件中链接图片是不可以的,因为大部分邮件服务商都会自动屏蔽带有外链的图片,他们不知道这些链接是否指向恶意网站。

发送附件的代码中,MIMEMultipartMIMETextplain改为html,然后修改msg.attach()中的第一个参数为HTML格式即可。需要修改的代码如下:

msg.attach(MIMEText('<html><body><h1>Hello</h1>' +
                    '<p><img src="cid:0"></p>' +
                    '</body></html>', 'html', 'utf-8'))

然后重新执行后可收到邮件:

在这里插入图片描述

5. 同时支持HTML和Plain格式

发送HTML邮件,收件人通过浏览器或Outlook之类的软件可正常浏览邮件内容,但收件人使用的设备太古老,查看不了HTML邮件,处理方法:在发送HTML的同时再附件一个纯文本,如果收件人无法查看HTML格式的邮件,就可以自动降级查看纯文本邮件

使用MIMEMultipart可组合一个HTML和Plain,注意指定subtypealternative

修改的代码如下:

# 邮件对象指定为 alternative
msg = MIMEMultipart('alternative')

# 编写两个邮件正文对象,这样保证无法查看HTML格式邮件的古老设备能收到纯文本的邮件内容
msg.attach(MIMEText('Hello,I am Jason', 'plain', 'utf-8'))
msg.attach(MIMEText('<html><body><h1>Hello</h1></body></html>', 'html', 'utf-8'))

因为现在的设备基本都支持HTML格式的邮件,所以收到的是第二个邮件对象的内容,第一个则过滤掉了:
在这里插入图片描述

6. 加密SMTP

使用标准的25端口连接SMTP服务器时,使用的是明文传输,发送邮件的整个过程可能会被窃听。要更安全的发送邮件,方法是:加密SMTP对话:先创建SSL安全连接,然后再使用SMTP协议发送邮件

举例:邮件服务商Gmail,提供的SMTP服务必须要加密传输,方法为:在创建SMTP对象后,立刻调用starttls()方法,就创建了安全连接。

smtp_server = 'smtp.gmail.com'
smtp_port = 587  # Gmail的SMTP端口是587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()  # 加密SMTP
  • 除了以上代码,其他都一样

小结

  • 使用Python的smtplib发送邮件十分简单,只要掌握了各种邮件类型的构造方法,正确设置好邮件头,就可以顺利发出
  • 构造一个邮件对象就是一个Message对象
  • 构造一个MIMEText对象就表示一个文本邮件对象
  • 构造一个MIMEImage对象就表示一个作为附件的图片
  • 要把多个对象组合起来,就用MIMEMultipart对象
  • MIMEBase可表示任何对象
  • 以上的继承关系如下:(这种嵌套关系可构造出任意复杂的邮件)
Message
← MIMEBase
	← MIMEMultipart
	← MIMENonMultipart
		← MIMEMessage
		← MIMEText
		← MIMEImage

2. POP3收取邮件

收取邮件:编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或手机上。

收取邮件最常用的协议是POP协议,目前版本是3,俗称POP3

收取邮件步骤

  • 使用Python内置的poplib模块把邮件的原始文本下载到本地
    • poplib模块,实现了POP3协议,可直接用来收邮件
  • 使用email解析原始文本,还原为邮件对象
    • email模块提供的各种类解析原始文本,把邮件变成可阅读的邮件对象

1. 通过POP3下载邮件

步骤说明:

  • 导入包

    import poplib
    from email.parser import Parser
    
  • 获取邮件地址、口令、pop3服务器地址(pop3地址如:pop3.qq.com

    email = input('Email:')
    password = input('Password:')
    pop3_server = input('POP3 server:')
    
  • 连接到pop3服务器

    server = poplib.POP3(pop3_server)
    
  • pop3欢迎语,可打印可不打印

    welcome_msg = server.getwelcome().decode('utf-8')
    
  • 身份认证

    server.user(email)
    server.pass_(password)
    
  • 邮件数量count和占用空间size,都在server.stat()

    count, size = server.stat()
    
  • server.list()包含三部分:邮件数量与占用空间resp、邮件列表mailsoctets暂不用管

    resp, mails, octets = server.list()
    
  • 邮件数量:len(server.list()[1]len(mails)

    count = len(server.list()[1])
    
  • 获取某封邮件的内容 server.retr(index)index1开始。要获取所有邮件,只需要循环使用retr()把每一封邮件内容拿到即可

    resp, lines, octets = server.retr(index)
    
  • lines存储邮件的原始文本的每一行,可获得整个邮件的原始文本

    msg_content = b'\r\n'.join(lines).decode('utf-8')
    
  • 解析出邮件

    msg = Parser().parsestr(msg_content)
    
  • 根据邮件索引号删除邮件 server.dele(index)

    server.dele(20)  # 把从最久的邮件开始算,序号为20的邮件被删除
    
  • 关闭连接

    server.quit()
    

代码如下:

import poplib
from email.parser import Parser

# 输入邮件地址、口令和POP3服务器地址
email = input('Email:')
password = input('Password:')
# 举例:pop3.qq.com、pop3.sina.com、pop3.163.com
pop3_server = input('POP3 server:')

# 连接到POP3服务器
server = poplib.POP3(pop3_server)
# 可以打开或关闭调试信息
# server.set_debuglevel(1)
# 可选:打印POP3服务器的欢迎文字
print(server.getwelcome().decode('utf-8'))

# 身份认证
server.user(email)
server.pass_(password)

# stat() 返回邮件数量和占用空间
print('Message: %s. Size: %s' % server.stat())
# list() 返回所有邮件的编号
resp, mails, octets = server.list()
# resp:总邮件数和总容量,类似于 server.stat()
# mails:列表形式返回所有邮件信息 [b'1 82923', b'2 2184', ...]
# octets:不用关心
print('Total:\nresp: {}\nmails: {}\noctets: {}'.format(resp, mails, octets))

# 获取最新一封邮件,注意索引号从1开始
index = len(mails)
resp2, lines, octets2 = server.retr(index)
# lines:邮件信息;resp2和octets2:不用关心
print('Last:\nresp: {}\nlines: {}\noctets: {}'.format(resp2, lines, octets2))

# lines存储了邮件的原始文本的每一行,可获得整个邮件的原始文本
msg_content = b'\r\n'.join(lines).decode('utf-8')
# 稍后解析出邮件
msg = Parser().parsestr(msg_content)

# 可以根据邮件索引号直接从服务器删除邮件
server.dele(22)  # 执行后就删掉了该index的邮件,邮箱可看

# 关闭连接
server.quit()


# 欢迎语 print(server.getwelcome().decode('utf-8')) 打印:
'''
+OK Welcome to coremail Mail Pop3 Server (163coms[10774b260cc7a37d26d71b52404dcf5cs])
'''

# print('Message: %s. Size: %s' % server.stat()) 打印:
'''
Message: 25. Size: 9006000
'''

# print('Total:\nresp: {}\nmails: {}\noctets: {}'.format(resp, mails, octets)) 打印:
'''
Total:
resp: b'+OK 22 8945506'
mails: [b'1 2330', b'2 2350', b'3 2360', b'4 2421', b'5 1726532', b'6 1726617', b'7 2712', b'8 2747', b'9 2823', b'10 1726567', b'11 1727191', b'12 1727240', b'13 29500', b'14 29488', b'15 29506', b'16 29506', b'17 29313', b'18 28949', b'19 29313', b'20 29192', b'21 29317', b'22 29532']
octets: 214
'''

# print('Last:\nresp: {}\nlines: {}\noctets: {}'.format(resp2, lines, octets2)) 打印:
'''
Last:
resp: b'+OK 29532 octets'
lines: [b'Received: from out180-163-24-185.mail.qq.com (unknown [180.163.24.185])', b'\tby mx19 (Coremail) with SMTP id RcCowADX6aCvc7hgl0v9FA--.38935S3;', b'\tThu, 03 Jun 2021 14:16:15 +0800 (CST)', b'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=qq.com; s=s201512;', 。。。 dki', b'\tm=pass header.i=@qq.com', b'X-Coremail-Antispam: 1Uf129KBjDUn29KB7ZKAUJUUUUU529EdanIXcx71UUUUU7v73', 。。。', b'', b'--===============3847937056631975014==--']
octets: 29532
'''

2. 解析邮件

解析邮件的过程和上一节构造邮件正好相反,过程如下:

  • 导入必要模块:
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
import smtplib
  • 将邮件内容解析为Message对象:
msg = Parser().parsestr(msg_content)
  • 这个Message对象本身可能是一个MIMEMultipart对象,即包含嵌套的其他MIMEBase对象,嵌套可能还不止一层,因此要递归的打印出Message对象的层次结构:
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)
                print('%s%s: %s' % ('  ' * indent, header, value))
            if (msg.is_multipart()):
                parts = msg.get_payload()
        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('%sText: %s' % ('  ' * indent, content + '...'))
        else:
            print('%sAttachment: %s' % ('  ' * indent, content_type))
  • 邮件的Subject或者Email中包含的名字都是经过编码后的str,要正常显示,就必须decode:
def decode_str(s):
    value, charset = decode_header(s)[0]
    if charset:
        value = value.decode(charset)
    return value
  • decode_header()返回一个list,因为像CcBcc这样的字段可能包含多个邮件地址,所以解析出来的会有多个元素。上面的代码只取了第一个元素。

  • 文本邮件的内容也是str,还需要检测编码,否则,非utf-8编码的邮件都无法正常显示:

def guess_charset(msg):
    charset = msg.get_charset()
    if charset is None:
        content_type = msg.get('Content-Type', '').lower()
        pos = content_type.find('charset=')
        if pos >= 0:
            charset = content_type[pos + 8:].strip()
    return charset
  • 此时可以试试收取一封邮件。先往自己邮箱发一封邮件,然后用浏览器登录邮箱,看看邮件收到没,如果收到了,就用Python程序把它收到本地:
    在这里插入图片描述
    运行程序,结果如下:
+OK Welcome to coremail Mail Pop3 Server (163coms[...])
Messages: 126. Size: 27228317

From: Test <xxxxxx@qq.com>
To: Python爱好者 <xxxxxx@163.com>
Subject: 用POP3收取邮件
part 0
--------------------
  part 0
  --------------------
    Text: Python可以使用POP3收取邮件……...
  part 1
  --------------------
    Text: Python可以<a href="...">使用POP3</a>收取邮件……...
part 1
--------------------
  Attachment: application/octet-stream

我们从打印的结构可以看出,这封邮件是一个MIMEMultipart,它包含两部分:第一部分又是一个MIMEMultipart,第二部分是一个附件。而内嵌的MIMEMultipart是一个alternative类型,它包含一个纯文本格式的MIMEText和一个HTML格式的MIMEText

小结

用Python的poplib模块收取邮件分两步:第一步是用POP3协议把邮件获取到本地,第二步是用email模块把原始邮件解析为Message对象,然后,用适当的形式把邮件内容展示给用户即可。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员老五

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值