%# -*- coding: utf-8-unix -*-

\section{SMTP客户端实现}
\label{sec:c:socket:s:smtp}

\subsection{实验目的}
\label{subsec:c:socket:s:smtp_object}

进一步理解和掌握基于Python进行TCP套接字编程的知识，
理解SMTP报文格式，能基于Python编写一个简单的SMTP客户端程序。

\subsection{实验内容}
\label{subsec:c:socket:s:smtp_content}

通过Python编写代码创建一个可以向标准电子邮件地址发送电子邮件的简单邮件客户端。
该客户端可以与邮件服务器创建一个TCP连接，
并基于SMTP协议与邮件服务器交互并发送邮件报文，
完成邮件发送后关闭连接。

\subsection{实验原理、方法和手段}
\label{subsec:c:socket:s:smtp_principle}

简单邮件传输协议（Simple Mail Transfer Protocol，SMTP）
是实现电子邮件收发的主要应用层协议，它基于TCP提供的可靠数据传输连接，
从发送方的邮件服务器向接收方的邮件服务器发送邮件。
注意，虽然一般情况下邮件总是从发送方的邮件服务器中发出，
但是工作在发送方邮件服务器上的发送程序是一个SMTP客户端，
因此一个完整的SMTP程序总有两个部分参与工作：
运行在发送方邮件服务器的SMTP客户端和运行在接收方邮件服务器的SMTP服务器。

SMTP是一个古老的应用层协议，
1982年在RFC文档 \href{https://tools.ietf.org/html/rfc821}{821}
中首次被定义，
然后在2001和2008年进行了两次更新，
分别为RFC \href{https://tools.ietf.org/html/rfc2821}{2821}
和RFC \href{https://tools.ietf.org/html/rfc5321}{5321}。
因此虽然SMTP拥有众多出色的性质，但也遗留了一些陈旧特征，
例如，它限制所有邮件报文的主体部分只能采用简单的7比特ASCII码表示。
所以在用SMTP传送邮件之前需要将二进制数据编码为ASCII码，
在传输后再进行解码还原为二进制数据
\footnote{SMTP存在一些服务扩展定义，
例如RFC \href{https://tools.ietf.org/html/rfc3030}{3030}
就定义了如何采用非ASCII的方式传送二进制数据}。 

假设存在一个发送方邮件服务器，主机名为company.com；
而对应的接收方邮件服务器的主机名为network.net。
SMTP客户端要从地址alice@company.com向地址bob@network.net发送报文
\texttt{Do you like ketchup？How about pickles？}。
以下流程展示了运行在发送方邮件服务器上的SMTP客户端（C）
和运行在接收方邮件服务器上的SMTP服务器（S）之间交换SMTP报文文本的实际通信过程：

\begin{code}[text]
  S: 220 network.net 
  C: HELO company.com
  S: 250 Hello company.com, pleased to meet you 
  C: MAIL FROM:<alice@company.com>
  S: 250 alice@company.com... Sender ok 
  C: RCPT TO:<bob@network.net>
  S: 250 bob@network.net... Recipient ok 
  C: DATA S:354 Enter mail, end with "."on a line by itself 
  C: Do you like ketchup?
  C: How about pickles?
  C: .
  S: 250 Message accepted for delivery 
  C: QUIT 
  S: 221 network.net closing connection
\end{code}

注意：以\texttt{C:}开头的ASCII码文本行是SMTP客户端通过TCP套接字发出的消息，
而以\texttt{S:}开头的ASCII码则是SMTP服务器通过TCP套接字发出的消息。

在这个例子中，SMTP客户端发送了5条命令：
\texttt{HELO}（是HELLO的缩写）、\texttt{MAIL FROM}、
\texttt{RCPT TO}、\texttt{DATA}以及\texttt{QUIT}，
这些命令都是自解释的，可以简单通过其英文含义理解。
在具体消息内容部分，客户端通过发送一个只包含一个句点的行向服务器指示消息内容的结束
（注意，在实际传输时，报文在结尾处会包含两个额外的不可打印字符：“0x0A”与“0x0D”。
分别表示回车“CR”和换行“LF”）。
服务器对客户端发出的每条命令都做出了回答，
其中每个回答含有一个回答码和一些英文解释，
其中的英文解释内容在实际使用时是可选的。

有一点需要注意，由于SMTP使用的是TCP连接，所以可以复用一个连接发送多封邮件。
在这种情况下，发送方在完成握手后会连续发送所有这些邮件，
对每个邮件，客户端用一个新的\texttt{MAIL FROM:<???@company.com>}开始，
并用一个独立的句点指示该邮件的结束，
然后在所有邮件发送完后才发送QUIT结束一次与服务器的连接。

\subsection{实验条件}
\label{subsec:c:socket:s:smtp_requirement}

\begin{itemize}
  \item 装有python环境的电脑一台；
  \item 已经正常运行的邮件服务器（开启非加密模式的SMTP支持）；
  \item 部分代码（\nameref{subsec:c:socket:s:smtp_additional}中已给出）；
  \item Python语言参考手册 -- TCP部分\footnote{可以参考Python3官方手册的
  \href{https://docs.python.org/zh-cn/3/library/socket.html}
  {套接字}部分，也可以查询其他相关手册}；
  \item SMTP协议参考手册
  \footnote{可以参考SMTP的\href{https://tools.ietf.org/html/rfc5321}{RFC文档}，
  也可以查询其他相关手册}。
\end{itemize}

\subsection{实验步骤}
\label{subsec:c:socket:s:smtp_procedure}

本实验\nameref{subsec:c:socket:s:smtp_additional}
一节中展示了一段SMTP客户端的框架代码，
学生需要逐步填充代码中不完善的部分，完成一个简单电子邮件客户端程序，
并通过向不同的账号发送电子邮件来测试程序。

\subsection{进阶任务}
\label{subsec:c:socket:s:smtp_rethink}

阅读相关文档，修改代码，使编写的程序可以发送包含图片等二进制数据的电子邮件。

\subsection{注意事项及有关说明}
\label{subsec:c:socket:s:smtp_notice}

由于SMTP协议较为传统，因此部分公开的邮件服务器默认情况下关闭了SMTP支持，
有些邮件服务器为了安全只支持工作在TLS安全连接上的SMTP。
因此为了方便测试，请选择支持非加密SMTP连接的服务器进行测试，
如果选择自行搭建SMTP服务器进行测试，
则需注意在设置中开启SMTP协议，并保证其工作在非TLS加密模式下。

\subsection{考核方法}
\label{subsec:c:socket:s:smtp_criterion}

本实验需提交一份实验报告和编写的代码文件。报告内容应当包括以下三个部分：
\begin{itemize}
  \item 代码的说明；
  \item 不同环境下代码运行的结果；
  \item 对结果的分析和总结体会。
\end{itemize}

本实验评分标准：

\begin{enumerate}
  \item 规定时间内完成实验报告20分；
  \item 代码正确运行，20分（不能正常运行0分）；
  \item 实验报告格式整洁，20分；
  \item 实验报告中详细记录了实验过程，在实验中所遇到的问题以及解决方法，20分；
  \item 实验报告中仔细分析了实验结果，并能提出自己的改进措施，20分。  
\end{enumerate}

\subsection{附件}
\label{subsec:c:socket:s:smtp_additional}

基于Python的SMTP客户端框架程序：

\begin{code}[python]
  from socket import * 
  msg = "\r\n I love computer networks!"
  endmsg = "\r\n.\r\n"
  # 选择一个邮件服务器 
  mailserver = #按需补充
  # 创建socket和邮件服务器建立TCP连接
  recv = clientSocket.recv(1024)
  print(recv)
  if recv[:3] != '220':
      print '220 reply not received from server.'
  # 发送HELO命令，打印服务器响应。
      heloCommand = 'HELO Alice\r\n'
      clientSocket.send(heloCommand)
      recv1 = clientSocket.recv(1024)
      print((recv1)
  
  if recv1[:3] != '250':
  print ('250 reply not received from server.')
  
  # 发送MAIL FROM命令，打印服务器响应。
  # 发送RCPT TO命令，打印服务器响应。
  # 发送DATA命令，打印服务器响应。
  # 发送邮件内容。
  # 消息以单个"."结束。
  # 发送QUIT命令，获取服务器响应。
\end{code}
