使用Socket API
编写一个SMTP
邮件服务器程序
- 作为
SMTP
服务器,接收邮件客户端程序的TCP
连接请求,接收SMTP
命令和邮件数据,将邮件保存在文件中; - 作为
SMTP
客户端,建立到实际邮件服务器的TCP
连接,发送SMTP
命令,将保存的邮件发送给实际邮件服务器; - 提供邮件差错报告:将实际邮件服务器的差错报告转发给邮件客户端软件;
- 支持一封邮件多个接收者,要求接收者属于不同的域(如
bupt.edu.cn
、163.com
、aliyun.com
,…); - 提供发件人和收件人Email地址格式检查功能,例如下列邮件地址是错误的:
chengli
,chengli@
,bupt.edu.cn
, ….
DEV C++
数据结构是整个程序的要点之一,程序维护者充分了解数据结构就可以对主要算法和处理流程有个基本的理解。下面描述程序中定义的全局变量和函数中的变量的变量名和变量所起的作用以及程序定义的标识符含义。
char ehlo[BUFSIZE]; //存客户端ehlo,用于连接真实的邮箱服务器
char mailFrom[BUFSIZE]; //存客户端邮箱
char rcptTo[5] [BUFSIZE]; //存目的邮箱
char clientIP[5] [BUFSIZE]; //客户端服务器ip地址
char ip[BUFSIZE]; //暂时存每次用户输入的IP地址
char data[BUFSIZE]; //存Data
char imf[BUFSIZE * 10]; //存邮件标准格式
char recvData[BUFSIZE]; //暂时存接收数据
char rcptJudge[BUFSIZE]; //用于判断目的邮箱数量
int i = 0; //记录目的邮箱数量
int j = 0; //记录目标邮箱数量
int error; //判断调用客户端函数是否成功
int now[6]; //记录时间
char fname[256] = {0}; //记录创建txt文件的文件名
FILE *fp; //打开文件
char nativeIP[BUFSIZE]; //存本机IP
char temp[3]; //存状态码
char username[BUFSIZE]; //存登陆用户名
char password[BUFSIZE]; //存登陆密码
r1-r28 //smtp协议通信过程中的状态码
BUFSIZE //为缓冲区大小4096
PORT //为端口号25
程序所设计的子程序所完成的功能,和每个参数的意义。
-
int main()
——主函数 功能:调用Server服务器函数并判断其调用是否成功;若成功则继续调用Client客户端函数并判断其是否调用成功。 -
int server()
——服务器函数 功能:起到SMTP服务器的作用,接收邮件客户端程序的TCP连接请求,接 收SMTP命令和邮件数据,将邮件保存在文件中,并生成日志。参数意义:
int Ret; //用于判断初始化WSADATA是否成功 WSADATA wsaData; //用来存储被WSAStartup函数调用后返回的WindowsSockets数据 SOCKET ListeningSocket; //用于监听客户机连接的套接字 SOCKET socketConnection; //用于与客户机连接的套接字 SOCKADDR_IN ServerAddr; //存储服务器地址 SOCKADDR_IN ClientAddr; //存储客户端地址 int ClientAddrLen = sizeof(ClientAddr); //存储客户端地址长度 int flag = 1; //判断是否可以连接
-
int client()
——客户端函数 功能:将程序作为SMTP客户端,建立到实际邮件服务器的TCP连接,发送 SMTP命令,将保存的邮件发送给实际邮件服务器;参数意义:
int Ret1; //用于判断初始化WSADATA是否成功 WSADATA wsaData1;//用来存储被WSAStartup函数调用后返回的Windows Sockets数据 SOCKET socketclient; //服务器的套接字 SOCKADDR_IN nativeAddr; //用于存储本地ip地址 SOCKADDR_IN clientAddr1; //用于存储客户端ip地址
-
int validEmail(char*addr)
——收发件邮箱地址合法性监测功能:提供发件人和收件人Email地址格式检查功能,例如下列邮件地址是错误的:
chengli
,chengli@
,bupt.edu.cn
, ….参数意义:
int colonAddr = 0; //记录“:”所在位置 int atAddr = 0; //记录“@”所在位置 int pointAddr = 0; //记录“.”所在位置 int bracketAddr = 0; //记录“>”所在位置 int error1 = 0; //返回值 unsigned int a = 0; //计数器
-
char* getIP()
功能:获取邮箱客户端ip地址 ;
参数意义:
char* hostIP; //函数内保存本地IP char hostName[256]; //保存邮箱客户端主机名称 struct hostent *hostEntry; //将邮箱客户端主机名称转换为IP*
-
*char* translateIP(char*mail)
功能:将源邮箱服务器地址转换为ip地址;
参数意义:
char ip[100] = { "smtp." }; //将smtp.存储下来方便找到服务器域名 char* IP; //保存邮箱服务器IP int atAddr = 0; //记录”@ ”所在位置 int bracketAddr = 0; //记录”> “所在位置 unsigned int x = 0; //计数器 int y = 5; //计数器 struct hostent *hostEntry; //找出客户端邮箱服务器的IP struct in_addr **addr_list; //将找到的IP进行格式转换
-
int time1()
——时间戳函数功能:获取当前时间,方便记录日志文件
参数意义:
time_t t; //用来存储时间 time(&t); //获取时间戳 lt = localtime(&t); //转为时间结构
-
void Error()
——判断错误类型功能:提供差错报告,将实际邮件服务器的差错报告转发给邮件客户端软件
画出流程图,描述算法的主要流程。
(备注:由于函数中使用了大量if语句,但是NS盒图很难画大量的if语句,故if语句用文字代替)
-
main()
——主函数实现要点:先后调用服务器客户端函数并判断是否调用成功
-
int server()
——服务器函数实现要点:通过socket进行服务器监听,客户端请求,连接确认三步客户端建立tcp连接,然后根据smtp的协议来进行通信,依次接收ehlo、auth login、用户名、密码、源邮箱地址、目的邮箱地址、邮件内容,获取邮件长度,最后关闭套接字释放资源。
其中使用了socket的常用函数如下:
int socket(int domain, int type, int protocol) //创建套接字 int bind(SOCKET socket, const struct sockaddr* address, socklen_t address_len) //绑定套接字 int recv(SOCKET socket, char FAR* buf, int len, int flags) //接收信息 int send (SOCKET socket, char FAR* buf, int len, int flags)//发送信息
-
int client()
——客户端函数实现要点:首先建立连接,获取到真正的客户端邮件服务器地址,开始作为客户端与之通信,依次发送ehlo、auth login、用户名、密码、客户端目的邮箱名称、data、imf、邮件末尾点号,最后发送quit通信结束。
其中使用了socket的常用函数如下
int socket(int domain, int type, int protocol)//创建套接字 int connect(SOCKET socket, const struct sockaddr* address, socklen_t address_len)//绑定套接字 int recv(SOCKET socket, char FAR* buf, int len, int flags) //接收信息 int send (SOCKET socket, char FAR* buf, int len, int flags)//发送信息
-
ValidEmail(char *)
——收发件邮箱地址合法性监测实现要点:根据标准email地址格式进行合法性监测;
对于所实现的功能,逐个进行测试,并将输出截图。
-
因未实现ssl所以不勾画ssl端口
-
将一封邮件发送给多个接收者,并且接收者属于不同的域
下图为程序输出截图
下图为程序运行后保存下来的以时间戳命名的日志文档
日志文档内容如下
目的邮箱2、3的发送日志和目的邮箱1相似,这里就不一一赘述,会把日志放在实验报告里面
用base64将邮件内容解码(解码网站:https://1024tools.com/base64) 得到真实邮件内容
下图为来自不同域的邮箱收到的邮件
-
邮件发送错误生成错误报告
-
发件人邮箱错误
由于foxmail不会出现发件邮箱错误,为此我们首先更改了一下代码,得到差错报告
时间戳日志内容
-
收件人邮箱错误
在时间戳日志中的内容为
-
接收
EHLO
错误由于foxmail一般不会出现接收”EHLO“错误,为此我们首先更改了一下代码,得到差错报告
时间戳日志内容
-
接收
AUTH LOGIN
错误由于foxmail一般不会出现接收
AUTH LOGIN
错误,为此我们首先更改了一下代码,得到差错报告时间戳日志内容
-
接收
DATA
错误由于foxmail一般不会出现接收
DATA
错误,为此我们首先更改了一下代码,得到差错报告时间戳日志内容
-
接收
.
错误由于foxmail一般不会出现接收
.
错误,为此我们首先更改了一下代码,得到差错报告时间戳日志内容
-
根据观察,发现foxmail发送
QUIT
后会直接退出,没有验证quit
的必要性,因此,没有QUIT
错误报告
-
-
本地服务器实现接收多个邮箱,即不固定用户名和密码,上述发送邮箱是QQ邮箱,接下来实现发送邮箱是163邮箱
提供了差错处理功能,具体内容如下
- 客户端发送邮件时,如果本机邮箱服务器未接收到命令,则会返回以下错误差错报告(差错报告截图已经出现在上面的图片中):
- 未接收到
EHLO——503 Can't receive EHLO\r\n
- 未接收到
AUTH LOGIN——503 Can't receive AUTH LOGIN\r\n
- 未接收到
DATA——503 Can't receive DATA\r\n
- 未接收到
. ——503 Can't receive ' . '\r\n
- 未接收到
- 在建立连接时如果出现错误会结束进程并且输出错误。
- 提供了Error函数来判断错误类型,并将其保存在日志文件中,如果在 socket 连接时出现问题则会返回错误代码并退出程序,方便进行调试。
- 程序中运用了大量的if-else语句来判断是否出错来。
-
程序并未完全遵照课堂上学习的 smtp 协议实现通信过程。
-
不同之处
学习的smtp协议客户端发送EHLO后直接发送发件邮箱和目的邮箱,即客户端接收到的250状态码后面不包含AUTH LOGIN。而我们的程序接收到客户端发送的EHLO后,发送的250状态码中包含AUTH LOGIN,即要求客户端接着发送用户名和密码。这样我们的程序不会仅限于接收一个邮箱,可以接收多个邮箱,如既可以接收QQ邮箱,也可以接收网易邮箱、Gmail邮箱,即没有把用户名和密码固定。
-
优点
- 在smtp协议基本功能实现的基础上,实现了差错报告,即用户可通过 POP3 协议登陆邮件服务器,从服务器上下载差错报告;
- 实现了检查邮箱地址是否合法;
- 支持多个收信人地址且所属域不同;
- 检查邮件发送过程中的多种错误;
- 可以根据发送客户端的邮箱格式,直接找出其真实客户端邮箱服务器的IP地址并与其进行连接,不用手动输入客户端邮箱服务器IP地址。
-
不足
- 没有成功实现SSL安全加密;
- 如果与真实的客户端邮箱服务器连接发送邮件的过程中出现错误后无法将错误发送给客户端,只能打印在日志里,虽然正常情况下不会出现错误;
- 工作效率不如实际的服务器;
- 差错处理的过程中返回的状态码和内容不一定全面。
不足
没有成功实现SSL安全加密。
b) 如果与真实的客户端邮箱服务器连接发送邮件的过程中出现错误后无法将错误发送给客户端,只能打印在日志里,虽然正常情况下不会出现错误。
c) 工作效率不如实际的服务器。
d) 差错处理的过程中返回的状态码和内容不一定全面。
-
完成本次实验的实际上机调试时间是多少?
本次实验的实际上机调试时间是10小时左右。
-
编程工具方面遇到了哪些问题?包括 Windows 环境和 VC 软件的安装问题。
- 使用Dev-C++时要更改编译器配置在连接器命令行时加上以下命令-static-libgcc -lwsock32 。
- 由于SSL配置环境比较复杂,开始并未使用SSL安全加密,使用的是DEV-C++编写的程序,编写结束后,放到VS-2017环境下,出现了很多错误,多数是函数的名称的更改和函数变量数目的更改。
- VS编译环境中SSL环境配置比较复杂,并且将原本的代码更改为SSL代码比较繁琐,这也是SSL未实现的原因。
-
编程语言方面遇到了哪些问题?包括C语言使用和对C语言操控能力上的问题
- 第一次使用C语言接触socket编程,在此过程中查找了大量的资料来使用socket的一些函数。
- C语言在实现从文件读取数据时与写入数据时比较复杂,查找了一些资料。
- 时间戳函数是自己写的一个函数,没发现C语言有自己的时间戳库函数。
- 子函数的套用容易出现逻辑错误。
- 返回值刚开始并未实现统一,导致写主函数时出现了逻辑错误。
-
协议方面遇到了哪些问题?
- 客户端在发送邮件内容时,会将邮件内容拆分为
DATA fragment
,...bytes
、imf
和.
,服务器端只判断最后的点号并且只在判断点号合法后才发送250 OK
。 - 服务器端发送的
250 Server ready
内容必须包含AUTH LOGIN
,客户端才会发送用户名和密码。 - 服务器在返回状态码命令时,需要在状态码后面添加其他字符否则会出错,无法接收客户端的下一条 smtp 命令。
- 客户端在发送邮件内容时,会将邮件内容拆分为
-
通过本次试验,你认为 SMTP 协议有哪些不足?有何改进思路?
-
不足
smtp 协议缺少安全性,smtp 协议除了用户名与密码和邮件内容需要通过base64码转换,其他信息均是明文传输,并且base64转换也很容易破译,容易受到第三方的攻击,被第三方截取邮件内容,或是篡改收件人,发件人的地址以及邮件内容,十分容易受到中间人攻击或者重放的攻击行为。
-
改进
可以将 smtp 发送的命令内容、邮件内容、地址都进行必要的加密。扩展改进等已有SSL,X2.5等增强版本存在。
-
-
总结本次实验,你在 C 语言方面,协议软件方面,理论学习方面,软件工程方面等哪些方面上有所提高?
- 第一次使用C语言编写一千多行的程序,通过此次smtp邮箱服务器编程实验,我们再次加深对C语言的学习,比如文件的读取、socket的一些编程函数的调用、程序逻辑规范等等。
- 在协议软件方面,我们对smtp协议有了更深的理解,对一些命令,返回状态码都理解得更透彻,巩固了学习到的邮件服务器如何工作,用户如何利用邮件服务器发邮件,以及通过 socket 建立一个连接等方面的知识,同时利用wireshark进行抓包深刻体会到smtp的安全性有待提升。
- 理论学习方面,对C/S模型和邮件传输协议的理解更加明了。
附:实验源码