Files
Computer_Network/Labs/Lab2/source/实验2_21281280_柯劲帆_物联网.md
2024-03-29 16:27:49 +08:00

19 KiB
Raw Permalink Blame History

实验报告

课程名称:计算机网络原理
实验题目:SMTP客户端编程实验
学号:21281280
姓名:柯劲帆
班级:物联网2101班
指导老师:常晓琳
报告日期:2024年3月29日

目录

[TOC]


1. 实验目的

本实验旨在运用各种编程语言实现基于 smtp 协议的 Email 客户端软件。能够对网络编程有进一步的理解和掌握,并能够理解 smtp 协议的细节。

  1. 选择合适的编程语言编程实现基于 smtp 协议的 Email 客户端软件。
  2. 安装 Email 服务器或选择已有的 Email 服务器,验证自己的 Email 客户端软件是否能进行正常的 Email 收发功能。

2. 实验环境

  • Server OSWSL2 Ubuntu-22.04 (Kernel: 5.15.146.1-microsoft-standard-WSL2)
  • 邮件服务商QQ邮箱
  • Pythonversion 3.11.5

3. 实验原理

3.1. STMP传输架构

用户与用户代理user agent打交道启动用户代理键入主题subject及报文的正文等。在一行上键入一个句点结束报文。用TCP进行的邮件交换是由报文传送代理MTAMessage Transfer Agent完成的在本实验中我使用的是QQ邮箱作为MTA。用户代理把邮件传给MTA由MTA进行交付。

3.2. STMP发送原理

最小SMTP实现支持8种命令。

  • HELO:标识自己。参数必须是完全合格的的客户主机名。
  • MAIL:标识出报文的发起人。
  • RCPT标识接收方。如果有多个接收方可以发多个RCPT命令。
  • DATA:发送邮件报文的内容。报文的末尾由客户指定,是只有一个句点的一行。
  • QUIT:结束邮件的交换。
  • RSET:异常中止当前的邮件事务并使两端复位。丢掉所有有关发送方、接收方或邮件的存储信息。
  • VRFY:使客户能够询问发送方以验证接收方地址,而无需向接收方发送邮件。通常是系统管理员在查找邮件交付差错时手工使用的。
  • NOOP除了强迫服务器响应一个OK应答码200不做任何事情。

在发送邮件时需要依次:

  1. 发送HELO命令标识自己;
  2. 发送MAIL标识报文发起人;
  3. 发送RCPT标识接收方;
  4. 发送DATA,含有邮件内容;
  5. 若以上任意步骤出现问题,发送RSET终止事务并复位;
  6. 结束发送使用QUIT

邮件内容的格式样例如下:

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
From: jingfan.ke@qq.com
To: jingfan.ke@qq.com
Subject: =?utf-8?b?U01UUCDpgq7ku7bmtYvor5U=?=

6L+Z5piv5LiA5bCB5rWL6K+V6YKu5Lu277yM5Y+R6YCB6IeqUHl0aG9u56iL5bqP44CC

内容采用MIME (Multipurpose Internet Mail Extensions) 标准:

  • Content-Type: text/plain; charset="utf-8":指定邮件正文的内容类型为纯文本(text/plain),并使用UTF-8编码。
  • MIME-Version: 1.0指明这封邮件使用的是MIME版本1.0标准。
  • Content-Transfer-Encoding: base64表明邮件正文的传输编码方式是Base64。
  • From: jingfan.ke@qq.com:邮件的发件人地址。
  • To: jingfan.ke@qq.com:显示邮件的收件人地址。
  • Subject: =?utf-8?b?U01UUCDpgq7ku7bmtYvor5U=?=:邮件主题,已被Base64方式编码。
  • 空行:分隔首部与正文。
  • 6L+Z5piv5LiA5bCB5rWL6K+V6YKu5Lu277yM5Y+R6YCB6IeqUHl0aG9u56iL5bqP44CC:邮件正文,已被Base64方式编码。

3.3. IMAP获取服务器邮件列表原理

IMAPInternet Message Access Protocol是一种邮件获取协议它允许邮件客户端访问并操作远程邮件服务器上的消息。与仅允许下载的POPPost Office Protocol不同IMAP提供了更复杂的邮件管理功能例如在服务器上保持邮件状态已读、未读、删除等以及支持在多个客户端之间同步邮件。

通过IMAP获取服务器邮件列表的步骤

  1. 建立连接并认证首先客户端使用IMAP协议通过SSL加密的方式连接到邮件服务器的IMAP服务并使用用户名和密码进行认证确保通信过程的安全性和用户身份的验证。
  2. 选择邮件文件夹认证成功后客户端选择要操作的邮件文件夹通常是“收件箱”。IMAP允许操作多个邮件文件夹包括用户自定义的文件夹。
  3. 搜索邮件:客户端可以根据需要搜索邮件。可以使用'ALL'参数来搜索所有邮件。IMAP支持多种搜索标准如日期、发件人、主题等允许灵活地获取邮件列表。
  4. 获取邮件搜索完成后服务器返回邮件的唯一标识符ID。然后客户端可以根据这些ID获取一封或多封邮件的完整内容或部分内容。可以通过遍历邮件ID列表并使用fetch命令按照RFC 822标准获取邮件的完整数据。
  5. 解析邮件内容邮件内容通常以MIME格式存储包含多部分内容如文本、HTML、附件等。客户端需要解析这些内容提取邮件的主体、主题、发件人等信息。可以使用email模块来解析邮件内容,并处理多部分消息和文本编码。
  6. 处理邮件:获取并解析邮件后,可以根据需要处理邮件,例如显示邮件列表、保存邮件到本地或对邮件进行标记处理。
  7. 断开连接:操作完成后,客户端发送logout命令来结束会话,并断开与服务器的连接。

通过以上步骤IMAP协议支持的邮件客户端能够高效地管理和操作服务器上的邮件支持复杂的邮件处理需求特别适用于需要在多个设备上访问和同步邮件的场景。

4. 实验过程

4.1. 发送邮件

4.1.1. 编写代码

首先写一个配置文件data/email_config.json指明发件人、收件人、邮件主题、邮件正文、SMTP服务器和密码

{
    "sender": "jingfan.ke@qq.com",
    "receiver": "jingfan.ke@qq.com",
    "subject": "SMTP 邮件测试",
    "body": "这是一封测试邮件发送自Python程序。",
    "smtp_server": "smtp.qq.com",
    "password": "xxxxxxxxxxxxxx"
}

该密码通过开通QQ邮箱的SMTP/POP3/IMAP服务获得。

在代码文件sender.python中:

  1. 从配置文件中提取所需的信息包括发件人、收件人、邮件主题、邮件正文、SMTP服务器和密码

    with open('./data/email_config.json', 'r') as config_file:
        config = json.load(config_file)
    
    sender = config['sender']
    receiver = config['receiver']
    subject = config['subject']
    body = config['body']
    smtp_server = config['smtp_server']
    password = config['password']
    
  2. 创建一个MIMEText对象,用于设置邮件的正文。邮件内容设为纯文本格式('plain'),字符集为'utf-8'

    message = MIMEText(body, 'plain', 'utf-8')
    
  3. 设置邮件头部信息,包括发件人、收件人和邮件主题。使用Header来确保头部信息能够正确处理字符编码。

    message['From'] = Header(sender)
    message['To'] = Header(receiver)
    message['Subject'] = Header(subject, 'utf-8')
    
  4. 尝试执行以下步骤来连接SMTP服务器并发送邮件

    • 使用SMTP_SSL连接到指定的SMTP服务器和端口这里使用465端口它通常用于SMTPS即加密SMTP

      server = smtplib.SMTP_SSL(smtp_server, 465)
      
    • 使用提供的发件人邮箱地址和密码登录SMTP服务器。

      server.login(sender, password)
      
    • 调用sendmail方法发送邮件。这里将邮件内容转换为字符串格式,并指定发件人和收件人地址。

      server.sendmail(sender, [receiver], message.as_string())
      

      sendmail方法在内部依次发送了HELO命令、MAIL命令、RCPT命令和DATA及内容,这部分将在运行实验部分详细叙述。

    • 如果邮件发送成功,则打印“邮件发送成功”的消息。

  5. 如果在邮件发送过程中遇到任何SMTPException异常,则捕获这个异常并打印“邮件发送失败”的消息,同时显示错误详情。

  6. 不论邮件发送成功与否,最后都会调用quit方法来关闭与SMTP服务器的连接。

    server.quit()
    

以上完整代码见附录。

4.1.2. 运行实验

将代码文件放在合适的位置,文件夹构建如下:

.
├── data
│   └── email_config.json
└── sender.py

使用Python运行代码

$ python sender.py 
邮件发送成功

发送目标邮箱即可收到邮件。

邮件截图

那么在发送邮件的过程中,究竟发送了什么具体的内容呢?

首先是邮件内容message字符串,打印内容为:

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
From: jingfan.ke@qq.com
To: jingfan.ke@qq.com
Subject: =?utf-8?b?U01UUCDpgq7ku7bmtYvor5U=?=

6L+Z5piv5LiA5bCB5rWL6K+V6YKu5Lu277yM5Y+R6YCB6IeqUHl0aG9u56iL5bqP44CC

经过对函数源码的深入解读,我发现sendmail()方法内部实现了发送邮件的主要逻辑:

  1. 调用了ehlo_or_helo_if_needed()方法发送了HELO命令:

    函数调用栈:ehlo_or_helo_if_needed() \rightarrow helo() \rightarrow putcmd() \rightarrow send() \rightarrow sock.sendall(),在sock.sendall()后添加打印语句print()打印socket发送的字符串为

    b'ehlo kkkkjf.\r\n'
    
  2. 调用了mail()方法发送了MAIL命令:

    函数调用栈:mail() \rightarrow putcmd() \rightarrow send() \rightarrow sock.sendall()打印socket发送的字符串为

    b'mail FROM:<jingfan.ke@qq.com> size=264\r\n'
    
  3. 对每个发送对象,调用一次rcpt()方法发送RCPT命令:

    函数调用栈:rcpt() \rightarrow putcmd() \rightarrow send() \rightarrow sock.sendall()打印socket发送的字符串为

    b'rcpt TO:<jingfan.ke@qq.com>\r\n'
    

    这里由于只有一个发送对象,所以只有一行。

  4. 调用了data()方法发送了DATA命令:

    函数调用栈:

    1. data() \rightarrow putcmd() \rightarrow send() \rightarrow sock.sendall()打印socket发送的字符串为

      b'data\r\n'
      
    2. data() \rightarrow send() $\rightarrow$ sock.sendall()打印socket发送的字符串为

      b'Content-Type: text/plain; charset="utf-8"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: base64\r\nFrom: jingfan.ke@qq.com\r\nTo: jingfan.ke@qq.com\r\nSubject: =?utf-8?b?U01UUCDpgq7ku7bmtYvor5U=?=\r\n\r\n6L+Z5piv5LiA5bCB5rWL6K+V6YKu5Lu277yM5Y+R6YCB6IeqUHl0aG9u56iL5bqP44CC\r\n.\r\n'
      

      这正是message字符串内容。

在打印邮件发送成功后,调用了server.quit(),探究源码后发现执行逻辑为:

函数调用栈:rcpt() \rightarrow docmd() \rightarrow putcmd() \rightarrow send() $\rightarrow$ sock.sendall()打印socket发送的字符串为

b'quit\r\n'

至此,邮件发送完毕。

4.2. 获取邮件列表

4.2.1. 编写代码

首先需要在data/email_config.json中添加IMAP服务器的地址。

{
    "sender": "jingfan.ke@qq.com",
    ...
    "imap_url": "imap.qq.com",
    "password": "xxxxxxxxxxxxxx"
}

接下来在代码文件receiver.python中:

  1. 配置加载

    从JSON文件中加载邮件客户端配置信息。使用json.load方法读取文件data/email_config.json中的配置信息包括用户的邮箱地址、密码、以及IMAP服务器的URL并将文件内容解析为Python字典。

  2. 连接和登录

    使用imaplib.IMAP4_SSL()方法建立与IMAP服务器的安全连接然后使用login()方法进行用户认证。保证后续操作的安全性和用户身份的验证。

  3. 邮件搜索和获取

    在成功登录后,客户端选择“收件箱”文件夹,并搜索所有邮件。通过search()方法获取到的邮件ID列表用于后续获取邮件的详细内容。

  4. 邮件解析

    遍历邮件ID列表使用fetch()方法获取每封邮件的完整数据RFC822。接着使用email.message_from_string()方法解析这些数据,提取邮件的发件人、主题和正文等信息。这一部分考虑到邮件可能是多部分格式,分别处理了邮件正文是单一部分和多部分的情况。

  5. 邮件信息保存

    将解析出的邮件信息保存到一个新的JSON文件中。使用json.dump()方法,将邮件数据写入文件,以便后续的查阅或分析。

  6. 输出和断开连接

    最后,打印最后一封邮件的发件人、主题和正文信息作为示例输出,并使用logout()方法断开与IMAP服务器的连接。

以上完整代码见附录。

4.2.2. 运行实验

代码文件和即将保存的邮件文件在文件夹中的位置:

.
├── data
│   └── emails.json
└── receiver.py

运行代码:

$ python receiver.py 
From:  jingfan.ke@qq.com
Subject:  SMTP 邮件测试
Body:  这是一封测试邮件发送自Python程序。

打印了最后一封邮件。

所有邮件列表保存在文件data/emails.json中,截图如下:

json保存邮件

5. 总结和感想

通过本次实验我深入学习和理解了SMTP协议的工作原理实际操作了如何使用Python编程语言实现基于SMTP协议的Email客户端软件。实验中我成功完成了邮件的发送和接收功能对于网络编程有了更加深刻的认识。

在实验的过程中我遇到了一些挑战比如理解SMTP和IMAP协议的细节、邮件内容的格式化、以及如何使用Python的smtpimaplib库来实现邮件的发送和接收。通过查询官方文档和一些技术博客,我逐步克服了这些难题,对这些协议的理解也更加深入了。

此外,我还学习到了如何使用json文件来管理配置信息,这种方法不仅使代码更加清晰,也让程序的可维护性和可扩展性大大提高。通过实验,我发现了编码实践对于加深理论知识的理解有着不可替代的作用。

总的来说,这次实验不仅让我对网络编程有了更进一步的了解,而且还提高了我解决实际问题的能力。通过动手实践,我学习到了许多课本之外的知识,这将对我未来的学习和研究工作大有裨益。我期待在未来能够继续探索更多关于计算机网络以及其他计算机科学领域的知识和技术。

6. 附录

sender.py

import smtplib
import json
from email.mime.text import MIMEText
from email.header import Header

# 从JSON文件中加载邮件配置
with open('./data/email_config.json', 'r') as config_file:
    config = json.load(config_file)

sender = config['sender']
receiver = config['receiver']
subject = config['subject']
body = config['body']
smtp_server = config['smtp_server']
password = config['password']

# 创建MIMEText对象设置邮件内容
message = MIMEText(body, 'plain', 'utf-8')
message['From'] = Header(sender)
message['To'] = Header(receiver)
message['Subject'] = Header(subject, 'utf-8')

try:
    # 连接SMTP服务器并发送邮件
    server = smtplib.SMTP_SSL(smtp_server, 465)  # 使用465端口
    server.login(sender, password)
    server.sendmail(sender, [receiver], message.as_string())
    print("邮件发送成功")
    server.quit()
except smtplib.SMTPException as e:
    print("邮件发送失败", e)

receiver.py

import imaplib
import email
import json
from email.header import decode_header

# 从JSON文件中加载配置信息
with open('./data/email_config.json', 'r') as config_file:
    config = json.load(config_file)

user = config['sender']
password = config['password']
imap_url = config['imap_url']

# 连接到IMAP服务器
mail = imaplib.IMAP4_SSL(imap_url)
mail.login(user, password)
mail.select('inbox')

# 搜索所有邮件
result, data = mail.search(None, 'ALL')
mail_ids = data[0]

id_list = mail_ids.split()
id_list.reverse()
emails = []

# 遍历邮件ID
for i in id_list:
    result, data = mail.fetch(i, '(RFC822)')
    raw_email = data[0][1]
    raw_email_string = raw_email.decode('utf-8',errors="ignore")
    email_message = email.message_from_string(raw_email_string)
    
    # 解析邮件内容
    mail_from = email_message['From']
    mail_subject = decode_header(email_message['Subject'])[0][0]
    if isinstance(mail_subject, bytes):
        mail_subject = mail_subject.decode('utf-8')
    mail_body = ''
    if email_message.is_multipart():
        for part in email_message.walk():
            ctype = part.get_content_type()
            cdispo = str(part.get('Content-Disposition'))
            if ctype == 'text/plain' and 'attachment' not in cdispo:
                mail_body = part.get_payload(decode=True).decode('utf-8')
                break
    else:
        mail_body = email_message.get_payload(decode=True).decode('utf-8')
    
    emails.append({'From': mail_from, 'Subject': mail_subject, 'Body': mail_body})

# 保存邮件列表到JSON文件
with open('./data/emails.json', 'w') as outfile:
    json.dump(emails, outfile, indent=4, ensure_ascii=False)

# 打印最后一封邮件的信息
if emails:
    print("From: ", emails[0]['From'])
    print("Subject: ", emails[0]['Subject'])
    print("Body: ", emails[0]['Body'])

mail.logout()