事先声明,整个过程以LOGIN认证方式为例,其他认证方式大同小异。按照时间顺序,主要分为22个步骤。
1、客户端TCP连接服务器25端口;
2、三次握手以后,连接建立成功,服务器主动推送服务就绪信息
网易邮箱一般都形如“220 163.com Anti-spam GT for Coremail System (163com[20111010])”;雅虎邮箱形如“220 smtp108.mail.gq1.yahoo.com ESMTP”;Google邮箱形如“220 mx.google.com ESMTP nw8sm917193igc.7”。其中220代表服务就绪,每一条服务就绪信息以“\r\n”为结尾标示符。
3、客户端向服务器说明身份
交代自己认证SMTP服务器的域名,例如雅虎邮箱的SMTP服务器为smtp.mail.yahoo.com,则发送“EHLO smtp.mail.yahoo.com\r\n”。
4、如果身份有效,则服务器进入等待认证状态,主动推送自身支持的所有SMTP认证方式
网易邮箱发送的内容如下:
表示其支持LOGIN、PLAIN两种认证方式;
雅虎邮箱发送的内容如下:
表示其支持LOGIN、PLAIN、XYMCOOKIE三种认证方式。
5、客户端判断自身是否支持服务器提供的SMTP认证方式
如果认证方式指定“auto”则采用服务端提供的第一个认证方式,如果指定其他方式,则判断服务端是否支持该方式,否则返回错误。
这一歩相当关键,因为客户端程序可以根据具体的认证方式加载相应插件来完成认证过程。
6、客户端向服务器请求认证
发送“AUTH LOGIN\r\n”。
7、如果认证请求合理,服务器将进入等待用户输入状态
发送“334 VXNlcm5hbWU6\r\n”,334表示等待客户端输入,VXNlcm5hbWU6表示等待输入用户名。
8、客户端向服务器发送转码后的用户名
发送经过Base64转码后的用户名(taotown)”dGFvdG93bg==\r\n“。
9、服务器再次进入等待用户输入状态
发送“334 UGFzc3dvcmQ6\r\n”,334表示等待客户端输入,UGFzc3dvcmQ6表示等待输入密码。
10、客户端向服务器发送转码后的密码
发送经过Base64转码后的密码(Haier)“SGFpZXI=\r\n”。
11、如果用户名或者密码出错,服务器将返回530错误,发送“530 Access denied\r\n”,表示认证失败。否则将返回235,发送“235 OK, Go ahead\r\n”,表示用户认证成功。
12、客户端告诉服务器邮件来自何方
发送“MAIL FROM:
13、如果合理,服务端返回250表示成功
发送“250 OK , completed\r\n”。
14、客户端告诉服务器邮件去往何地
发送“RCPT TO:
15、如果合理,服务器返回250表示成功
发送“250 OK , completed\r\n”。
16、客户端告诉服务器自己准备发送邮件正文
发送“DATA\r\n”。
17、服务器返回354,表示自己已经作好接受邮件的准备
发送“354 Start Mail. End with CRLF.CRLF\r\n”,提醒客户端开始发送邮件并以“.”结束。
18、客户端发送邮件正文(如果正文过长,可以分多次发送)
发送
19、客户端发送完正文以后,紧接着发送结束符
发送“.”。
20、如果合理,服务端返回250表示成功
发送“250 OK , completed\r\n”。
21、邮件发送结束,客户端请求断开连接
发送“QUIT\r\n”。
22、服务器返回211,提示断开申请被采纳,并主动断开连接,整个邮件发送过程结束。
发送“221 Service Closing transmission\r\n”。
附:如果服务端传过来的错误码后面紧跟这”-“,则说明该次消息分了很多节,直到最后一节没有”-“为止。
资料二
在以前接触的项目中,一直都是在做网站时用到了发送mail 的功能,在asp 和.net 中都有相关的发送mail 的类, 实现起来非常简单。最近这段时间因工作需要在C++ 中使用发送mail 的功能,上网搜了一大堆资料,终于得以实现,总结自己开发过程中碰到的一些问题,希望对需的人有所帮助, 由于能力有限, 文中不免有些误解之处,望大家能指正!!
其实,使用C++ 发送mail 也是很简的事, 只需要了解一点SMTP 协议和socket 编程就OK 了, 网络上也有很多高人写好的mail 类源码,有兴趣的朋友可以下载看看.
1. SMTP 常用命令简介
1). SMTP 常用命令
HELO/EHLO 向服务器标识用户身份
MAIL 初始化邮件传输
mail from:
RCPT 标识单个的邮件接收人;常在MAIL 命令后面
可有多个rcpt to:
DATA 在单个或多个RCPT 命令后,表示所有的邮件接收人已标识,并初始化数据传输,以. 结束。
VRFY 用于验证指定的用户/ 邮箱是否存在;由于安全方面的原因,服务器常禁止此命令
EXPN 验证给定的邮箱列表是否存在,扩充邮箱列表,也常被禁用
HELP 查询服务器支持什么命令
NOOP 无操作,服务器应响应OK
QUIT 结束会话
RSET 重置会话,当前传输被取消
如你对SMTP 命令不了解,可以用telnet 命令登陆到smtp 服务器用help 命令进行查看:
220 tdcsw.maintek.corpnet.asus ESMTP Sendmail 8.13.8/8.13.8; Sat, 9 Jan 2010 10:
45:09 +0800
help
214-2.0.0 This is sendmail
214-2.0.0 Topics:
214-2.0.0 HELO EHLO MAIL RCPT DATA
214-2.0.0 RSET NOOP QUIT HELP VRFY
214-2.0.0 EXPN VERB ETRN DSN AUTH
214-2.0.0 STARTTLS
214-2.0.0 For more info use “HELP
214-2.0.0 To report bugs in the implementation see
214-2.0.0 http://www.sendmail.org/email-addresses.html
214-2.0.0 For local information send email to Postmaster at your site.
214 2.0.0 End of HELP info
2).SMTP 返回码含义
* 邮件服务返回代码含义
* 500 格式错误,命令不可识别(此错误也包括命令行过长)
* 501 参数格式错误
* 502 命令不可实现
* 503 错误的命令序列
* 504 命令参数不可实现
* 211 系统状态或系统帮助响应
* 214 帮助信息
* 220 服务就绪
* 221 服务关闭传输信道
* 421 服务未就绪,关闭传输信道(当必须关闭时,此应答可以作为对任何命令的响应)
* 250 要求的邮件操作完成
* 251 用户非本地,将转发向
* 450 要求的邮件操作未完成,邮箱不可用(例如,邮箱忙)
* 550 要求的邮件操作未完成,邮箱不可用(例如,邮箱未找到,或不可访问)
* 451 放弃要求的操作;处理过程中出错
* 551 用户非本地,请尝试
* 452 系统存储不足,要求的操作未执行
* 552 过量的存储分配,要求的操作未执行
* 553 邮箱名不可用,要求的操作未执行(例如邮箱格式错误)
* 354 开始邮件输入,以. 结束
* 554 操作失败
* 535 用户验证失败
* 235 用户验证成功
* 334 等待用户输入验证信息 for next connection>;
3) SMTP 命令应用
我们下需使用telnet 命令实现smtp 邮件的发送,具体操作如下:
220 tdcsw.com ESMTP Sendmail 8.13.8/8.13.8; Wed, 23 Dec 2009 18
:18:18 +0800
HELO tdcsw
250 tdcsw.com Hello x-128-101-1-240.ahc.umn.edu [128.101.1.240], pleased to meet you
MAIL FROM:[email protected]
250 2.1.0 [email protected]… Sender ok
RCPR TO:[email protected]
250 2.1.5 [email protected]… Recipient ok
DATA
354 Enter mail, end with “.” on a line by itself
SUBJECT:HELLO
HI:
HAR are you?
.
250 2.0.0 nBNAIIG4000507 Message accepted for delivery
quit
221 2.0.0 tdcsw.maintek.corpnet.asus closing connection
Connection to host lost.
2. 用C++ 实现Mail 发送
为了便于理解, 在此就不封装Mail 类了, 而是以过程式函数方式给出.
1). 首先需要建立TCP 套接字, 连接端口依服务器而定,SMTP 服务默认端口为25, 我们以 默认端口为例
WSADATA wsaData;
int SockFD;
WSAStartup(MAKEWORD(2,2), &wsaData);
SockFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
ServAddr.sin_family = AF_INET;
ServAddr.sin_addr.s_addr = inet_addr (“192.168.1.1”); //192.168.1.1 为服务器地址
ServAddr.sin_port = htons(25);
connect(SockFD, (struct sockaddr *)&ServAddr, sizeof(ServAddr));
2). 发送SMTP 命令及数据
const char HEADER[] = “HELO smtpSrv/r/n”
“MAIL FROM: [email protected]/r/n”
“RCPT TO: [email protected]/r/n”
“DATA/r/n”
“FROM: [email protected]/r/n”
“TO: [email protected]/r/n”
“SUBJECT: this is a test/r/n”
“Date: Fri, 8 Jan 2010 16:12:30/r/n”
“X-Mailer: shadowstar’s mailer/r/n”
“MIME-Version: 1.0/r/n”
“Content-type: text/plain/r/n/r/n”;
//send HEADER
send(SockFD, HEADER, strlen(HEADER), 0);
const char CONTENT[]=”this is content./r/n”;
//send CONTENT
send(SockFD, CONTENT, strlen(CONTENT), 0);
send(SockFD, “./r/n”, strlen(“./r/n”), 0); //end
send(SockFD, “QUIT/r/n”, strlen(“QUIT/r/n”), 0); //quit
mail 发送的功能基本上就完成了, 当然, 如果是应用的话还是需要很多改动的地方的, 比如说添加附件等.
3). 附件功能
要使用SMTP 发送附件, 需要对SMTP 头信息进行说明, 改变Content-type 及为每一段正文添加BOUNDARY名, 示例如下:
“DATA/r/n”
“FROM: [email protected]/r/n”
“TO: [email protected]/r/n”
“SUBJECT: this is a test/r/n”
“Date: Fri, 8 Jan 2010 16:12:30/r/n”
“X-Mailer: shadowstar’s mailer/r/n”
“MIME-Version: 1.0/r/n”
“Content-type: multipart/mixed; boundary=/”#BOUNDARY#/”/r/n/r/n”;
// 正文
“–#BOUNDARY#/r/n”
“Content-Type: text/plain; charset=gb2312/r/n”
“Content-Transfer-Encoding: quoted-printable/r/n”
邮件正文……….
// 附件
“/r/n–#BOUNDARY#/r/n”
“Content-Type: application/octet-stream; name=att.txt/r/n”
“Content-Disposition: attachment; filename=att.txt/r/n”
“Content-Transfer-Encoding: base64/r/n”
”/r/n”
附件正信息(base64 编码)…..
Base64 编码函数在网络上很容易找到, 这里就不给出源码了, 如需要支持HTML 格式而又不知道如何写这些头信息, 可以用outlook 或foxmail 写一封支持HTML 格式的mail, 查看其原文信息, 依照相同的格式发送就行了.
4). 实现抄送及密送
在SMTP 命令集中并没有RCPT CC 或RCPT BCC 相关命令, 那要如何来实现抄送和密送功能呢?
在网络上找到这样一句话: “ 所有的接收者协商都通过RCPT TO 命令来实现,如果是BCC ,则协商发送后在对方接收时被删掉信封接收者”, 开始一直不明白这句话是什么意思? 后来通看查看foxmail 的邮件原文发现:
Date: Wed, 6 Jan 2010 12:11:48 +0800
From: “carven_li” < carven_li @smtp.com>
To: “carven”
Cc: “sam”
“yoyo”
BCC: “clara”
Subject: t
X-mailer: Foxmail 5.0 [cn]
Mime-Version: 1.0
Content-Type: multipart/mixed;
boundary=”=====001_Dragon237244850520_=====”
才恍然大悟, 所谓的” 协商” 应该就是指发送方在Data 中指定哪些为CC, 哪些为BCC, 默认情况下什么都不写, 只发送第一个RCPT TO 的mail, 其他的都被过滤掉
3. SMTP身份认证 5). DIGEST-MD5认证方式
DIGEST-MD5认证也是Challenge/Response的方式, 与CRAM-MD5相比, 它的Challenge信息更多, 其Response计算方式也非常复杂, 我在测试时也是以认证失败而告终, 只是将在网上找到的资料整理于此, 能为后来研究的人多提供点资料, 或者有兴趣的朋友们可以和我一起讨论下.
我们先看下DIGEST-MD5认证发送响应信息:
DIGEST-MD5服务器格式说明(见RFC 2831 Digest SASL Mechanism Mai 2000):
digest-challenge =
1 # (Reich | Nonce | qop-Optionen | schal | MAXBUF | charset
Algorithmus | Chiffre-opts | auth-param)
realm = “Reich” “=” < “> Reich-Wert <”>
Reich-Wert = qdstr-val
Nonce = “Nonce” “=” < “> Nonce-Wert <”>
Nonce-Wert = qdstr-val
qop-options = “qop” “=” < “> qop-Liste <”>
qop-list = 1 # qop-Wert
qop-Wert = “auth” | “auth-int” | “auth-conf” |
Token
stale = “veraltete” “=” “true”
MAXBUF = “MAXBUF” “=” MAXBUF-Wert
MAXBUF-Wert = 1 * DIGIT
charset = “charset” = “” UTF-8 “
algorithm = “Algorithmus” “=” “md5-sess”
Chiffre-opts = “Chiffre” “=” < “> 1 # Null-Wert <”>
Chiffre-value = “3des” | “des” | “RC4-40” | “RC4” |
“RC4-56” | Token
auth-param = Token “=” (token | quoted-string)
DIGEST-MD5客户端响应格式说明(见RFC 2831 Digest SASL Mechanism Mai 2000):
digest-response = 1 # (Benutzername | Reich | Nonce | cnonce |
Nonce-count | qop | digest-uri | Antwort |
MAXBUF | charset | Chiffre | authzid |
auth-param)
username = “username” = “<”> username-Wert < “>
Benutzernamen-Wert = qdstr-val
cnonce = “cnonce” “=” < “> cnonce-Wert <”>
cnonce-Wert = qdstr-val
Nonce-count = “nc” “=” nc-Wert
nc-Wert = 8LHEX
qop = “qop” “=” qop-Wert
digest-uri = “digest-uri” = “<”> digest-uri-value < “>
digest-uri-value = serv-type “/” host [ “/” serv-name] //eg: smtp/mail3.example.com/example.com
serv-type = 1 * ALPHA //www for web-service, ftp for ftp-dienst, SMTP for mail-versand-service …
host = 1 * (ALPHA | DIGIT | “-” | “.”)
serv-name = host
response = “Antwort” “=” Response-Wert
response-value = 32LHEX
LHEX = “0” | “1” | “2” | “3” |
“4” | “5” | “6” | “7” |
“8” | “9” | “a” | “b” |
“c” | “d” | “e” | “f”
cipher = “Chiffre” “=” Null-Wert
authzid = “authzid” “=” < “> authzid-Wert <”>
authzid-Wert = qdstr-val
其各字段具体含义见相关文档, 这里只介始几个需要用到的字段是如何产生的, C/S响应示例如下:
S: realm=”elwood.innosoft.com”,nonce=”OA6MG9tEQGm2hh”,qop=”auth”,
algorithm=md5-sess,charset=utf-8
C: charset=utf-8,username=”chris”,realm=”elwood.innosoft.com”,
nonce=”OA6MG9tEQGm2hh”,nc=00000001,cnonce=”OA6MHXh6VqTrRk”,
digest-uri=”imap/elwood.innosoft.com”,
response=d388dad90d4bbd760a152321f2143af7,qop=auth
S: rspauth=ea40f60335c427b5527b84dbabcdfffd
The password in this example was “secret”.
从这个示例可以看出, 客户端返回的信息比服务器端发送过来的多了以下几个:
username, nc, cnonce, digest-uri和respone
username就不用说了, nc是8位长的16进制数字符串,统计客户端使用nonce发出请求的次数(包含当前请求),例示我们可以设为”00000001”, cnonce是是用了4个随机数组成一个8位长16进制的字符串,digest-uri是由在realm前加上请求类型(如http, smtp等), response是一个32位长的16进制数组, 计算公式如下:
If the “qop” value is “auth” or “auth-int”:
request-digest = <”> < KD ( H(A1), unq(nonce-value)
“:” nc-value
“:” unq(cnonce-value)
“:” unq(qop-value)
“:” H(A2)
) <”>
If the “qop” directive is not present (this construction is for
compatibility with RFC 2069):
request-digest =
<”> < KD ( H(A1), unq(nonce-value) “:” H(A2) ) >
<”>
See below for the definitions for A1 and A2.
Read more: http://www.faqs.org/rfcs/rfc2617.html#ixzz0c4s8ck3F
KD(secret,data)表示分类算法,其中data指数据,secret表示采用的方法.如果表示校验和算法时,data要写成H(data);而unq(X)表示将带引号字符串的引号去掉。
对于”MD5” 和”MD5-sess” 算法:
H(data) = MD5(data)
和
KD(secret, data) = H(concat(secret, “:”, data))
如果”algorithm”指定为”MD5”或没有指定,A1计算方式如下:
A1 = unq(username-value) “:” unq(realm-value) “:” passwd
//Password为用户密码
如果”algorithm”指定为”MD5-sess”, 则需要nonce和cnonce的参与:
A1 = H(unq(username-value) “:” unq(realm-value) “:” passwd )
“:” unq(nonce-value) “:” unq(cnonce-value)
如果”qop”没有指定或指定为”auth”, A2计算方式如下:
A2 = Method “:” digest-uri-value
如果”qop”没有指定或指定为”auth int”, A2计算方式如下:
A2 = Method “:” digest-uri-value “:” H(entity-body)
Method是http请求时的方法(post,get), 由于英文水平比较差, 很多都看不明白, 有兴趣的朋友可以自己去看看原文(http://www.faqs.org/rfcs/rfc2617.html), 这里还提供了DIGEST验证的代码:
File “digcalc.h”:
#define HASHLEN 16
typedef char HASH[HASHLEN];
#define HASHHEXLEN 32
typedef char HASHHEX[HASHHEXLEN+1];
#define IN
#define OUT
/* calculate H(A1) as per HTTP Digest spec */
void DigestCalcHA1(
IN char * pszAlg,
IN char * pszUserName,
IN char * pszRealm,
IN char * pszPassword,
IN char * pszNonce,
IN char * pszCNonce,
OUT HASHHEX SessionKey
);
/* calculate request-digest/response-digest as per HTTP Digest spec */
void DigestCalcResponse(
IN HASHHEX HA1, /* H(A1) */
IN char * pszNonce, /* nonce from server */
IN char * pszNonceCount, /* 8 hex digits */
IN char * pszCNonce, /* client nonce */
IN char * pszQop, /* qop-value: “”, “auth”, “auth-int” */
IN char * pszMethod, /* method from the request */
IN char * pszDigestUri, /* requested URL */
IN HASHHEX HEntity, /* H(entity body) if qop=”auth-int” */
OUT HASHHEX Response /* request-digest or response-digest */
);
File “digcalc.c”:
#include
#include
#include
#include “digcalc.h”
void CvtHex(
IN HASH Bin,
OUT HASHHEX Hex
)
{
unsigned short i;
unsigned char j;
for (i = 0; i < HASHLEN; i++) {
j = (Bin[i] >> 4) & 0xf;
if (j <= 9)
Hex[i*2] = (j + ‘0’);
else
Hex[i*2] = (j + ‘a’ - 10);
j = Bin[i] & 0xf;
if (j <= 9)
Hex[i*2+1] = (j + ‘0’);
else
Hex[i*2+1] = (j + ‘a’ - 10);
};
Hex[HASHHEXLEN] = ‘/0’;
};
/* calculate H(A1) as per spec */
void DigestCalcHA1(
IN char * pszAlg,
IN char * pszUserName,
IN char * pszRealm,
IN char * pszPassword,
IN char * pszNonce,
IN char * pszCNonce,
OUT HASHHEX SessionKey
)
{
MD5_CTX Md5Ctx;
HASH HA1;
MD5Init(&Md5Ctx);
MD5Update(&Md5Ctx, pszUserName, strlen(pszUserName));
MD5Update(&Md5Ctx, “:”, 1);
MD5Update(&Md5Ctx, pszRealm, strlen(pszRealm));
MD5Update(&Md5Ctx, “:”, 1);
MD5Update(&Md5Ctx, pszPassword, strlen(pszPassword));
MD5Final(HA1, &Md5Ctx);
if (stricmp(pszAlg, “md5-sess”) == 0) {
MD5Init(&Md5Ctx);
MD5Update(&Md5Ctx, HA1, HASHLEN);
MD5Update(&Md5Ctx, “:”, 1);
MD5Update(&Md5Ctx, pszNonce, strlen(pszNonce));
MD5Update(&Md5Ctx, “:”, 1);
MD5Update(&Md5Ctx, pszCNonce, strlen(pszCNonce));
MD5Final(HA1, &Md5Ctx);
};
CvtHex(HA1, SessionKey);
};
/* calculate request-digest/response-digest as per HTTP Digest spec */
void DigestCalcResponse(
IN HASHHEX HA1, /* H(A1) */
IN char * pszNonce, /* nonce from server */
IN char * pszNonceCount, /* 8 hex digits */
IN char * pszCNonce, /* client nonce */
IN char * pszQop, /* qop-value: “”, “auth”, “auth-int” */
IN char * pszMethod, /* method from the request */
IN char * pszDigestUri, /* requested URL */
IN HASHHEX HEntity, /* H(entity body) if qop=”auth-int” */
OUT HASHHEX Response /* request-digest or response-digest */
)
{
MD5_CTX Md5Ctx;
HASH HA2;
HASH RespHash;
HASHHEX HA2Hex;
// calculate H(A2)
MD5Init(&Md5Ctx);
MD5Update(&Md5Ctx, pszMethod, strlen(pszMethod));
MD5Update(&Md5Ctx, “:”, 1);
MD5Update(&Md5Ctx, pszDigestUri, strlen(pszDigestUri));
if (stricmp(pszQop, “auth-int”) == 0) {
MD5Update(&Md5Ctx, “:”, 1);
MD5Update(&Md5Ctx, HEntity, HASHHEXLEN);
};
MD5Final(HA2, &Md5Ctx);
CvtHex(HA2, HA2Hex);
// calculate response
MD5Init(&Md5Ctx);
MD5Update(&Md5Ctx, HA1, HASHHEXLEN);
MD5Update(&Md5Ctx, “:”, 1);
MD5Update(&Md5Ctx, pszNonce, strlen(pszNonce));
MD5Update(&Md5Ctx, “:”, 1);
if (*pszQop) {
MD5Update(&Md5Ctx, pszNonceCount, strlen(pszNonceCount));
MD5Update(&Md5Ctx, “:”, 1);
MD5Update(&Md5Ctx, pszCNonce, strlen(pszCNonce));
MD5Update(&Md5Ctx, “:”, 1);
MD5Update(&Md5Ctx, pszQop, strlen(pszQop));
MD5Update(&Md5Ctx, “:”, 1);
};
MD5Update(&Md5Ctx, HA2Hex, HASHHEXLEN);
MD5Final(RespHash, &Md5Ctx);
CvtHex(RespHash, Response);
};
File “digtest.c”:
#include
#include “digcalc.h”
void main(int argc, char ** argv) {
char * pszNonce = “dcd98b7102dd2f0e8b11d0f600bfb0c093”;
char * pszCNonce = “0a4f113b”;
char * pszUser = “Mufasa”;
char * pszRealm = “[email protected]”;
char * pszPass = “Circle Of Life”;
char * pszAlg = “md5”;
char szNonceCount[9] = “00000001”;
char * pszMethod = “GET”;
char * pszQop = “auth”;
char * pszURI = “/dir/index.html”;
HASHHEX HA1;
HASHHEX HA2 = “”;
HASHHEX Response;
DigestCalcHA1(pszAlg, pszUser, pszRealm, pszPass, pszNonce,
pszCNonce, HA1);
DigestCalcResponse(HA1, pszNonce, szNonceCount, pszCNonce, pszQop,
pszMethod, pszURI, HA2, Response);
printf(“Response = %s/n”, Response);
};
到这里,关于使用SMTP发送mail就结束了, 由于水平有限, 有很多地方可能讲不够透彻!!!