基础知识:
SMTP
1、概述
SMTP被用来在因特网上传递电子邮件。文件RFC821规定了该协议的所有细节。
协议的原理很简单。无非是一个客户端计算机向服务器发送命令,然后服务器向客户端计算机返回一些信息。客户端发送的命令以及服务器的回应都是字符串。
首先要与邮件服务器连接,服务器将返回文本。该文本包含一个三位数的代码及描述,例如:
220-ns.cinfo.ru Sendmail 8.6.12/8.6.9 ready at Wed, 22 Apr 1998 22:54:41 +0300
220 ESMTP spoken here
这些描述可能会因服务器而异。只须要知道代码所代表的意思就行了。代码220表示成功建立连接,服务器等待你的第一个命令。
向服务器传递的第一个命令是HELO. 该命令包含一个参数,即你的邮箱名。
HELO oleg
注意: 在RFC821中,HELO是一个可选择性命令,如果服务器不要求该命令的话,你可以把它忽略掉。
如果命令成功,服务器会返回一个代码为250的回应。
下一步用MAIL FROM命令告诉服务器你想发一封邮件。该命令以发信人的邮件地址为参数。
MAIL FROM: [email protected]
发完命令后,如果服务器返回一个代码为250回应,你就可以向服务器发送RCPT TO命令了。
RCPT TO命令以收信人地址为参数,是告诉服务器你想将邮件发到收信人地址处。
RCPT TO: [email protected]
如果你想将邮件发给多个收件人的话。你需要多次使用RCPT TO命令,对每个命令,服务器都会返回代码为250的回应。
现在可以向服务器发送邮件正文了。
用DATA命令告诉服务器以下的内容为邮件正文。在你从服务器收到代码为354的回应后,你就可以发送邮件正文了。邮件按行发送,每行邮件以一个无回车的换行符结束,只须按回车键就行了。下面是一个例子:
Subject: My first e-mail message.
First line of a message.
Second line.
注意上面最后一行的最后一个字符是一个小数点。这是正文结束的标志。 服务器收到这个标志后,就会立即返回一个代码为250的回应以及该邮件的唯一ID号。
250 WAA10568 Message accepted for delivery
任务完成了,可以继续发送下封邮件,也可以断开同服务器的连接。如果要断开同服务器的连接就用QUIT命令。在这种情况下,服务器会返回一个代码为221的回应并断开连接。
QUIT
221 ns.cinfo.ru closing connection
2、工作机制
SMTP设计基于以下通信模型:针对用户的邮件请求,发送SMTP建立与接收SMTP之间建立一个双向传送通道。接收SMTP可以是最终接收者也可以是中间传送者。SMTP命令由发送SMTP发出,由接收SMTP接收,而应答则反方面传送。
一旦传送通道建立,SMTP发送者发送MAIL命令指明邮件发送者。如果SMTP接收者可以接收邮件则返回OK应答。SMTP发送者再发出RCPT命令 确认邮件是否能接收到。如果SMTP接收者接收,则返回OK应答;如果不能接收到,则发出拒绝接收应答(但不中止整个邮件操作),双方将如此重复多次。当 接收者收到全部邮件后会接收到特别的序列,如果接收者成功处理了邮件,则返回OK应答
SMTP提供传送邮件的机制,如果接收方与发送方连接在同一个传送服务下时,邮件可以直接由发送方主机传送到接收方主机;或者,当两者不在同一个传送服务下时,通过中继SMTP服务器传送。为了能够对SMTP服务器提供中继能力,它必须拥有最终目的主机地址和邮箱名称。
MAIL命令参数是回复路径,它指定邮件从何处来;而RCPT命令的参数是转发路径的,它指定邮件向何处去。向前路径是源路径,而回复路径是返回路径(它用于发生错误时返回邮件)。
3、基本命令(所有的命令都是四个字母组成)
发 送邮件操作涉及到不同的数据对象,它们由不同的参数相互连接。回复路径就是MAIL命令的参数,而转发路径则是RCPT命令的参数,邮件日期是DATA命 令的参数。这些参数或者数据对象必须跟在命令后。这种模式也就要求有不同的缓冲区来存储这些对象,也就是说,有一个回复路径缓冲区,一个转发路径缓冲区, 一个邮件内容缓冲区。特定的命令产生自己的缓冲区,或使一个或多个缓冲的内容被清除
(0)HELO(HELLO)
HELO〈SP〉〈domain〉〈CRLF>
通知服务器客户的身份。
此命令用于向接收SMTP确认发送SMTP。参数域包括发送SMTP的主机名。接收SMTP通过连接确认命令来向发送SMTP确认接收SMTP。引命令和OK响应确认发送和接收SMTP进入了初始状态,也就是说,没有操作正在执行,所有状态表和缓冲区已经被子清除。
(1)MAIL
MAIL FROM:
此 命令用于开始将邮件发送到一个多个邮箱中。参数域包括回复路径。返回路径中包括了可选的主机和发送者邮箱列表。当有主机列表时,它是一个回复路径源,它说 明此邮箱是由在表中的主机一一传递发送(第一个主机是最后一个接收到此邮件的主机)过来的。此表也有作向发送者返回非传递信号的源路径。因为每个传递主机 地址都被加在此表起始处,它就必须使用发送IPCE而不是接收IPCE(如果它们不是一个IPCE的话)清楚的名称。一些出错信息的回复路径可能就是空 的。
此命令清除回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区,并且将此命令的回复路径信息插入到回复路径缓冲区中。
(2)RCPT(recipient)
RCPT TO:
此命令用于确定邮件内容的唯一接收者;多个接收者将由多个此命令指定。转发路径中包括一个可选的主机和一个必须的目的邮箱。
此命令给出向前路径标识接收者,如果命令被接收,接收方返回一个250 OK应答,并存储向前路径。如果接收者未知,接收方会返回一个550 Failure应答。此过程可能会重复若干次。
不仅包括邮件,它是主机和目的邮箱的路由表,在其中的第一个主机就是接收命令的主机。
(3)DATA
DATA
如果命令被接收,接收方返回一个354 Intermediate应答,并认定以下的各行都是信件内容。当信件结尾收到并存储后,接收者发送一个250 OK应答。因为邮件是在传送通道上发送,因此必须指明邮件内容结尾,以便应答对话可以重新开始。SMTP通过在最后一行仅发送一个句号来表示邮件内容的结 束,在接收方,一个对用户透明的过程将此符号过滤掉,以不影响正常的数据。
注意:邮件内容包括如下提示:Date, Subject, To, Cc, From。
邮件内容指示符确认邮件操作并告知接收者可以存储和再发送数据了。如果此命令被接收,接收方返回一个250 OK应答。DATA命令仅在邮件操作未完成或源无效的情况下失败。
(4)RSET
RSET
此命令指示当送邮件操作将被放弃。任何保存的发送者,接收者和邮件内容应该被抛弃,所有缓冲区和状态表应该被清除,接收方必须返回OK应答。
(5)SEND
SENDFROM:〈reverse-path>
此命令用于开始一个发送命令,将邮件发送到一个或多个终端上。参数域包括了一个回复路径,此命令如果成功就将邮件发送到终端上了。
回复路径包括一个可选的主机列表和发送者邮箱。当出现主机列表时,表示这是一个传送路径,邮件就是经过这个路径上的每个主机发送到这里的(列表上第一个 主机是最后经手的主机)。此表用于返回非传递信号到发送者。因为每个传递主机地址都被加在此表起始处,它就必须使用发送IPCE而不是接收IPCE(如果 它们不是一个IPCE的话)清楚的名称。一些出错信息的回复路径可能就是空的。
此命令清除回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区,并且将此命令的回复路径信息插入到回复路径缓冲区中。
(6)SOML(send or mail)
SOMLFROM:〈reverse-path>
此命令用于开始一个邮件操作将邮件内容传送到一个或多个终端上,或者传送到邮箱中。对于每个接收者,如果接收者终端打开,邮件内容将被传送到接收者的终端上,否则就送到接收者的邮箱中。参数域包括回复路径,如果成功地将信息送到终端或邮箱中此命令成功。
回复路径包括一个可选的主机列表和发送者邮箱。当出现主机列表时,表示这是一个传送路径,邮件就是经过这个路径上的每个主机发送到这里的(列表上第一个主机是最后经手的主机)。
此命令清除回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区,并且将此命令的回复路径信息插入到回复路径缓冲区中。
(7)SAML(send and mail)
SAMLFROM:〈reverse-path>
此命令用于开始一个邮件操作将邮件内容传送到一个或多个终端上,并传送到邮箱中。如果接收者终端打开,邮件内容将被传送到接收者的终端上和接收者的邮箱中。参数域包括回复路径,如果成功地将信息送到邮箱中此命令成功。
回复路径包括一个可选的主机列表和发送者邮箱。当出现主机列表时,表示这是一个传送路径,邮件就是经过这个路径上的每个主机发送到这里的(列表上第一个主机是最后经手的主机)。
此命令清除回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区,并且将此命令的回复路径信息插入到回复路径缓冲区中。
(8)VRFY(verify vt.证实,查证;证明)
VRFY〈string〉
此命令要求接收者确认参数是一个用户。如果这是(已经知道的)用户名,返回用户的全名和指定的邮箱。此命令对回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区没有影响。
(9)EXPN(expand vt.扩大;使膨胀)
EXPN〈string〉
此命令要求接收者确认参数指定了一个邮件发送列表,如果是一个邮件发送列表,就返回表中的成员。如果这是(已经知道的)用户名,返回用户的全名和指定的邮箱。此命令对回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区没有影响。
(10)NOOP
NOOP
此命令不影响任何参数和已经发出的命令。它只是说明没有任何操作而不是说明接收者发送了一个OK应答。此命令对回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区没有影响。
(11)QUIT
QUIT
此命令指示接收方必须发送OK应答然后关闭传送信道。接收方在接到QUIT命令并做出响应之前不应该关闭通信信道。发送方在发送QUIT命令和接收到响 应之前也不应该关闭信道。即使出错,也不应该关闭信道。如果连接被提前关闭,接收方应该象接收到RSET命令一样,取消所有等待的操作,但不恢复原先已经 做过的操作。而发送方应该象接收到暂时错误(4XX)一样假定命令和操作仍在支持之中。
4、 SMTP响应
对SMTP命令的响应是多样的,它确定了在邮件传输过程中请求和处理的同步,也保证了发送SMTP知道接收SMTP的状态。每个命令必须有且只有一个响应。
SMTP响应由三位数字组成,其后跟一些文本。数字帮助决定下一个应该进入的状态,而文本对人是有意义的。三位的响应已经包括了足够的信息,不用再阅读 文本,文本可以直接抛弃或者传递给用户。特别的是,文本是与接收和环境相关的,所以每次接收到的文本可能不同。在附录E中可以看到全部的响应码。正规的情 况下,响应由下面序列构成:三位的数字,,一行文本和一个,或者也可以是一个多行响应。只有EXPN和HELP命令可以导致多行应答,然而,对所有命令,多行响应都是允许的。
三 位的应答码每一位都有特定的意义。每一位应答表示是否是成功的,失败的或未完成的。通过这一位,不复杂的SMTP发送就可以决定下一步的操作,如果发送方 希望大概了解究竟出了什么问题,它可以检测第二位,而第三位则保存了最后更完整的信息。也就是说,从第一位到第三位,接收方可以一步比一步精确地确定接收 方的状态。
(1)对于第一位有五种可能的表示代表不同的意义:
1yz 部分完成应答
命令被接受,但是要求的操作被中止,原因在应答码中。发送方应该再次发送另一命令指明是否继续操作,或者放弃操作。
2yz 全部完成应答
要求的操作已经完成,可以开始另一个新的请求。
3yz 需要近一步信息的部分完成应答
命令被接受,但是要求的操作被中止,需要接收进一步的信息。发送方应该发送另一条命令指明进一步的信息。
4yz 暂时未完成应答
命令未被接受,要求的操作也未执行,但是发生错误的状态是暂时的,可以再一次请求操作。发送者应该返回命令序列的开始命令(如果有的话)。很难解释这个 暂时的意义,特别对于两个不同的站点来说。区别应答是属于些类还是下一类的方法是:如果能够不加任何改变地重复的再一次发送命令,就是本类的,如果不是, 就是下一类(5yz)的。
5yz 永久未完成应答
命令未被接受,要求的操作未完成。发送对命令的重复不起作用。即使一些出错条件已经改变,但是用户已经不希望重试,而希望在未来的某个时间再进行操作
(2)应答的第二位的意义有以下几类:
x0z 语法:此类型的应答是针对以下情况的:语法错误;符合语法但命令不存在功能;未完成或冗余的命令。
x1z 信息:此类型的应答是用于请求信息的,如状态或帮助信息。
x2z 连接:此类型的应答是关于传输信道的。
x3z 未使用。
x4z 未使用。
x5z 邮件系统:此类型的应答指明接收方邮件系统关于请求传送或其它操作的状态的。
解决方法:
为了适应有些传统系统 会把8bits首位过滤掉的问题,
邮件 协议里专门添加了base64和quoted-printable两种编码方式,把多字节字符转换成7bits字符,
具体的概念以及编码规则 本文就不说了。
按照rfc的规范, “简单email测试”这几个字会被转成
=?gb2312?B?vPKlpWVtYWlssuLKlA==?=
抽象的说,是以 "=?" 开头 , "?="结束,中间 两个问号夹着编码规则, Q表示quoted-printable , B 表示 base64 。 如果按照Q或B规则重新解码,邮件原字就能正确显示。
但是有些软件 会有一个不同于rfc要求的处理,即在使用base64编码时对一个字符串进行分段处理。比如上面的“简单email测试”, 就会分成三段 “简单” “email" "测试”。
按照这样处理的编码就成了出来的
=?gb2312?B?vPKlpQ==?=email=?gb2312?B?suLKlA==?=
大家应该都能看出来,区别在于处理时对于天然就是7bits的字符,不作任何编码转换,保持原貌,其他的分段编码。
MIME对信头字段的扩展
MIME 对RFC822的字段补充了一些信头字段,这些字段通常是在发送电子邮件的软件在创建电子邮件时产生的,接受电子邮件的软件提取其中的字段得到有用的信息
MIME补充的信头字段:
MIME-Version字段用于标志使用的MIME版本号,这是为了将来增加版本号解决兼容的问题。该字段是MIME信件唯一必须要求出现的字段。
目前只有一个MIME版本在使用,一般加入一下字段:
MIME-Version: 1.0
Content-Type是MIME中的主要字段,描述特定MIME实体中包含的数据。
这个字段有3部分:前两部分组成媒体类型和1个可选分号分开的参数列表。如:
Content-Type:text/plain;charset="us-ascii"
其中text是媒体类型主类别,plain指媒体类型的附加层次类别。charset="us-ascii"是可选的参数列表。又如: Content-Type:text/html;charset=gb2312
指定媒体的主类型是text,但文本内容是HTML格式文件。使用字符集是GB2312。
Content-Transfer-Encoding,许多数据格式可以包含在信件中允许的字符范围之外的字节值,而且含有超过允许长度的数据行。一些数据格式的定义甚至没有行的概念。Content-Transfer-Encoding字段解决这些问题.
该字段有5个值:7bit,8bit,binary,base64和quoted-printable.每个值都不区分大小写。符合MIME规范的邮件处理程序必须能对这些编码正确处理。
一个简单的会话 例子: 1:AfxSocketInit(NULL) 2:m_Socket.Create() 3:m_Socket.Connect(LPCSTR(temp),25) 4:virtual void OnReceive(int nErrorCode);等待服务器回应接受m_Socket.Receive(sBuffer,sizeof (sBuffer)); 5:发送m_Socket.Send("HELO smtp.sohu.com/r/n" ,len ) 返回4 6:发送m_Socket.Send("MAIL FROM: [email protected]/r/n" ,len ) 返回4 7:发送m_Socket.Send("RCPT TO: [email protected]/r/n" ,len ) 返回4 8:发送m_Socket.Send("DATA/r/n" ,len ) 返回4 9:发送m_Socket.Send("text/r/n/r/n./r/n" ,len ) 返回4 (注意".") 9:发送m_Socket.Send("QUIT/r/n" ,len ) 返回4 10 结束 程序: 1定义一个CESocket类 继承于CSocket负责相应接受数据消息,CESocket初始化Init m_iCount 是一个命令发送的序号 //初始话 void CESocket::Init(CEmailSendDlg *dlg) { m_dlg=dlg; m_iCount=0; } CESocket接受数据相应函数OnReceive,调用主对话框的接受消息处理函数,同时增加m_iCount //接受数据响应函数,调用主对话框的接受消息处理函数,同时增加m_iCount void CESocket::OnReceive(int nErrorCode) { m_dlg->ReceiveMessage(m_iCount); m_iCount++; CSocket::OnReceive(nErrorCode); } 2:对话框的消息处理函数: void CEmailSendDlg::OnSend() { GetDlgItemText(IDC_SMTP_ADDRESS, m_sSendString[0]); GetDlgItemText(IDC_FROM_ADDRESS, m_sSendString[1]); GetDlgItemText(IDC_TO_ADDRESS, m_sSendString[2]); m_sSendString[0].TrimLeft(" "); m_sSendString[0].TrimRight(" "); m_sSendString[1].TrimLeft(" "); m_sSendString[1].TrimRight(" "); m_sSendString[2].TrimLeft(" "); m_sSendString[2].TrimRight(" "); if(m_sSendString[0].IsEmpty()) { AfxMessageBox("请输入SMTP服务器地址(IP)"); return; } if(m_sSendString[1].IsEmpty()) { AfxMessageBox("请输入你的EMail地址"); return; } if(m_sSendString[2].IsEmpty()) { AfxMessageBox("请输入收件人的EMail地址"); return; } m_sSendString[0]="HELO "+m_sSendString[0]+"/r/n"; m_sSendString[1]="MAIL FROM: "+m_sSendString[1]+"/r/n"; m_sSendString[2]="RCPT TO: "+m_sSendString[2]+"/r/n"; m_sSendString[3]="DATA/r/n"; GetDlgItemText(IDC_EMAIL_CONTENT,m_sSendString[4]); m_sSendString[4]=m_sSendString[4]+"/r/n/r/n./r/n"; m_sSendString[5]="QUIT/r/n"; CString temp; GetDlgItemText(IDC_SMTP_ADDRESS,temp); if(m_Socket.Connect(LPCSTR(temp),25)==FALSE) { } m_sReceivedData=""; } 当客户端连接大SMTP服务器后,smtp会发送一段欢迎码然后客户端和服务器就可以按照SMTP命令进行会话 直到信件发送结束 void CEmailSendDlg::ReceiveMessage(int count) { if(count>=6) { AfxMessageBox("Unknown Received Data"); m_Socket.Close(); return; } char sBuffer[255]; int len=m_Socket.Receive(sBuffer,sizeof(sBuffer)); sBuffer[len]=NULL; m_sReceivedData=m_sReceivedData+sBuffer; m_sReceivedData=m_sReceivedData+m_sSendString[count]; m_Socket.Send(m_sSendString[count],m_sSendString[count].GetLength()); }