随笔-145  评论-173  文章-70  trackbacks-0
      五一放假了,没有到哪里去玩,虽然说还是很多事情要做,不过先做做这个再说。于是花了大概一天半的时间,搞定了这个小的程序,也算是回报吧!以后会继续完善和补充的。
      话说上次腾讯2面的时候我表现太不好了,伤心啊~~~所以从现在起,要在忙中抽时间来继续Coding,增强实力。估计过段时间的百度和其他实习不会去了,研究生真的很重要,所以要好好准备考研了,加油!
       不说废话了,上笔记:


SMTP邮件发送剖析

封装之后的类如下:



// MySmtp.cpp: implementation of the MySmtp class.
//
/**/
//////////////////////////////////////////////////////////////////////
 
#include 
"stdafx.h"
#include 
"MailExam.h"
#include 
"MySmtp.h"
#include 
"ZBase64.h"
 
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
 
 
#define COPYRIGHT "Smtp Client"         // 版权信息
#define BOUNDARY "www.hust.edu.cn"                                // 边界字符串
/**///////////////////////////////////////////////////////////////////////
// Construction/Destruction
/**/
//////////////////////////////////////////////////////////////////////
 
MySmtp::MySmtp()
{
 
}
 
MySmtp::
~MySmtp()
{
 
}
 
bool MySmtp::CreateSocket()        //创建一个Socket
{
         
if(WSAStartup(0x0101&m_WSADATA) != 0)        //至此socket版本是2.2,第一个参数也可以是MAKEWORD( 2, 2 )
         {
                  ReleaseSocket();
                  
return false;             //创建失败
         }
         
if( (m_SOCKET= socket(AF_INET,SOCK_STREAM, 0)) == INVALID_SOCKET){
                  ReleaseSocket();
                  
return false;
         }
         
return true;
}
 
//通过和比较码比较来发现是否响应成功
bool MySmtp::CheckResponse(const char* RecvCode)
{
         
//将收到的和对应的码字比较,判断是否发生错误
         char buf[1024= {0};
         
if(recv(m_SOCKET,buf,1024,0== SOCKET_ERROR)
                  
return false;
         
else 
         {
                  
return buf[0== RecvCode[0&& buf[1== RecvCode[1]
                  
&&buf[2== RecvCode[2? true : false;
         }
}
 
bool MySmtp::Connect(const string SmtpAddr,const int Port)
{
         
if(!CreateSocket())
                  
return false;
         
//得到主机(要发送给的SMTP地址如smtp.sina.com.cn)地址,并将相应的信息写入m_HOSTENT,
         
//就是在那个字符串中查找到相应信息并且构造一个结构体HOSTENT(这个记录了很多信息,不仅仅是地址)
         if((m_HOSTENT = gethostbyname((SmtpAddr.c_str()))) == NULL) //将传入的主机参数给m_HOSTENT
                  return false;
//    AfxMessageBox(m_HOSTENT->h_name);    //调试用的,找到HOSTENT的结构内容
//    AfxMessageBox(*m_HOSTENT->h_aliases);
         if(m_HOSTENT->h_addr_list[0== NULL)     //地址列表为空
         {
                  ReleaseSocket();
                  
return false;
         }
//    AfxMessageBox(m_HOSTENT->h_addr_list[0]);
//    AfxMessageBox(m_HOSTENT->h_addr_list[1]);
         memset(&m_SOCKADDR_IN,0,sizeof(m_SOCKADDR_IN));
         
//将这个SOCKET和主机地址联系起来,其实WinSoket中,m_HOSTENT 和m_SOCKADDR_IN都是表示的主机地址,也就是目的地的地址
         
//
         m_SOCKADDR_IN.sin_family = AF_INET;
         m_SOCKADDR_IN.sin_addr.S_un.S_addr 
= *(ULONG *) m_HOSTENT->h_addr_list[0];
         m_SOCKADDR_IN.sin_port 
= htons(Port);
//    u_long tmp = *(ULONG *) m_HOSTENT->h_addr_list[0];
//     char newstring[30];
//    sprintf(newstring,"%d",tmp);
//    AfxMessageBox(newstring);
 
         
//进行连接
         if(connect(m_SOCKET,(sockaddr *)&m_SOCKADDR_IN,sizeof(m_SOCKADDR_IN)) == SOCKET_ERROR)
         {
                  ReleaseSocket();
                  
return false;
         }
         
if(!CheckResponse("220")) return false;                 //服务准备就绪
         
         
//向服务器发送"HELO "+服务器名
         string strTmp="HELO "+SmtpAddr+"\r\n";
         
if(send(m_SOCKET,strTmp.c_str(),strTmp.length(),0== SOCKET_ERROR) 
         {
                  ReleaseSocket();
                  
return false;
         }
         
if(!CheckResponse("250")) return false;                 //请求操作就绪
         
         
return true;
}
 
void MySmtp::ReleaseSocket()
{
         shutdown(m_SOCKET,SD_BOTH);
         closesocket(m_SOCKET);
         WSACleanup();
 
}
bool MySmtp::SendData(const string SendFrom, const string SendToList, 
                                              
const string SenderName, const string ReceiverName, 
                                              
const string Subject, const string Content )
{
         
if(SendFrom.empty())
                  
return false;    //源地址是空的
         if(SendToList.empty()) 
                  
return false;    //目的地址为空
         
         
string strTmp;
         ZBase64 base64;
         
         
//发送MAIL FROM:<abc@xyz.com>
         strTmp="MAIL FROM:<"+SendFrom+">\r\n";
         
if(send(m_SOCKET,strTmp.c_str(),strTmp.length(),0== SOCKET_ERROR)
         {
                  ReleaseSocket();
                  
return false;
         }
         
if(!CheckResponse("250")) return false;
         
         
//发送RCPT To:<abc@xyz.com>
         strTmp="RCPT To:<"+SendToList+">\r\n";
         
if(send(m_SOCKET,strTmp.c_str(),strTmp.length(),0== SOCKET_ERROR)
         {
                  ReleaseSocket();
                  
return false;
         }
         
if(!CheckResponse("250")) return false;
 
         
         
//发送"DATA\r\n"
         if(send(m_SOCKET,"DATA\r\n",strlen("DATA\r\n"),0== SOCKET_ERROR)
         {
                  ReleaseSocket();
                  
return false;
         }
         
if(!CheckResponse("354")) return false;
         
         
//"Mail From:SenderName<xxx@mail.com>\r\n"
         strTmp="From:"+SenderName+"<"+SendFrom+">\r\n";
         
         
//"Subject: 邮件主题\r\n"
         strTmp+="Subject:"+Subject+"\r\n";
         
         
//"MIME_Version:1.0\r\n"
         strTmp+="MIME_Version:1.0\r\n";
         
         
//"X-Mailer:Smtp Client By xxx"//版权信息
         strTmp+="X-Mailer:"; strTmp+=COPYRIGHT; strTmp+="\r\n";
         
         
//"MIME_Version:1.0\r\n"
         strTmp+="MIME_Version:1.0\r\n";
         
         
//"Content-type:multipart/mixed;Boundary=xxx\r\n\r\n";
         strTmp+="Content-type:multipart/mixed;Boundary=";
         strTmp
+=BOUNDARY;
         strTmp
+="\r\n\r\n";
         
         
//先将HEADER部分发送过去
         if(send(m_SOCKET,strTmp.c_str(),strTmp.length(),0== SOCKET_ERROR)
         {
                  ReleaseSocket();
                  
return false;    
         }
         
         
//邮件主体
         strTmp="--";
         strTmp
+=BOUNDARY;
         strTmp
+="\r\n";
         strTmp
+= "Content-type:text/plain;Charset=gb2312\r\n";
         strTmp
+="Content-Transfer-Encoding:8bit\r\n\r\n";
         
         
//邮件内容
         strTmp+=Content+"\r\n\r\n";
         
         
//将邮件内容发送出去
         if(send(m_SOCKET,strTmp.c_str(),strTmp.length(),0== SOCKET_ERROR)
         {
                  ReleaseSocket();
                  
return false;    
         }
         strTmp
="--";
         strTmp
+=BOUNDARY;
         strTmp
+="--\r\n.\r\n";
         
         
if(send(m_SOCKET,strTmp.c_str(),strTmp.length(),0== SOCKET_ERROR)
         {
                  ReleaseSocket();
                  
return false;
         }
         
if(!CheckResponse("250")) return false;
         
         
//退出
         if(send(m_SOCKET,"QUIT\r\n",strlen("QUIT\r\n"),0== SOCKET_ERROR)
         {
                  ReleaseSocket();
                  
return false;
         }
         
if(!CheckResponse("221")) return false;
         
         ReleaseSocket();
         
return true;
}
bool MySmtp::Validate(const string Username,const string Password)
{
         ZBase64 base64;
         
         
//发送"AUTH LOGIN"
         if(send(m_SOCKET,"AUTH LOGIN\r\n",strlen("AUTH LOGIN\r\n"),0== SOCKET_ERROR)
         {
                  ReleaseSocket();
                  
return false;
         }
         
if(!CheckResponse("334")) return false;
         
         
//发送经base64编码的用户名
         string strUserName=base64.Encode((unsigned char *)Username.c_str(),Username.length())+"\r\n";
         
if(send(m_SOCKET,strUserName.c_str(),strUserName.length(),0== SOCKET_ERROR)
         {
                  ReleaseSocket();
                  
return false;
         }
         
if(!CheckResponse("334")) return false;
         
         
//发送经base64编码的密码
         string strPassword=base64.Encode((unsigned char *)Password.c_str(),Password.length())+"\r\n";
         
if(send(m_SOCKET,strPassword.c_str(),strPassword.length(),0== SOCKET_ERROR)
         {
                  ReleaseSocket();
                  
return false;
         }
         
if(!CheckResponse("235")) return false;
         
         
return true;
}

 

 

 

具体如何调用的是用这个实现:


void CMailExamDlg::OnOK() 
{
         
// TODO: Add extra validation here
         MySmtp smtp;
         CString strSmtpAddr,strAccount,strPasswrod;
         GetDlgItemText(IDC_EDIT1,strSmtpAddr);
         GetDlgItemText(IDC_EDIT2,strAccount);
         GetDlgItemText(IDC_EDIT3,strPasswrod);
         
if(!smtp.Connect((LPSTR)(LPCTSTR)strSmtpAddr,25)){
                  AfxMessageBox(
"连接服务器失败!"); return ;    
         }
         
         
//验证用户名密码
         if(!smtp.Validate((LPSTR)(LPCTSTR)strAccount,(LPSTR)(LPCTSTR)strPasswrod)){
                  AfxMessageBox(
"用户名或密码失败!"); return ;        
         }
         CString SendFrom,SendToList,SenderName,RecevierName,Subject,Content;
         GetDlgItemText(IDC_EDIT4,SendFrom);
         GetDlgItemText(IDC_EDIT5,SendToList);
         GetDlgItemText(IDC_EDIT6,Content);
         GetDlgItemText(IDC_EDIT7,SenderName);
         GetDlgItemText(IDC_EDIT8,RecevierName);
         GetDlgItemText(IDC_EDIT9,Subject);
         
//发送
         if(!smtp.SendData((LPSTR)(LPCTSTR)SendFrom,
                  (LPSTR)(LPCTSTR)SendToList,
                  (LPSTR)(LPCTSTR)SenderName,
                  (LPSTR)(LPCTSTR)RecevierName,
                  (LPSTR)(LPCTSTR)Subject,
                  (LPSTR)(LPCTSTR)Content))
         {
                  AfxMessageBox(
"邮件发送失败!"); 
                  
return ;
         }
         AfxMessageBox(
"邮件发送成功!");
//     CDialog::OnOK();
}

 

实际上本质就是,1,连接Connect,2,验证账户密码Validate,3,发送数据

 

 

最核心的部分:如何实现MySmtp?

首先看看类视图,看看这个封装的类到底实现了哪些功能:

                               

 

 

在这个部分,关键的是:connect,Validate,checkResponse,SendData,CreateSocket这些函数,分别来说明:

CreateSocket: 创建Socket,利用Socket来编写邮件客户端,就是需要利用到Socket来作为一个门户啊,关于socket的解释,这里不再多说,由于是用的Windows下面的平台,所以需要用到的WinSock来编写,对应的就需要一定的规则。具体来说就是需要WSAStartup来

这个函数是应用程序应该第一个调用的Winsock API 函数,以完成一系列初始化的工作。必不可少!

其次,就是需要完成真正的创建,所以调用Socket函数来实现,创建后的返回值就是一个SOCKET对象,需要保存它,因此专门定义一个类成员m_SOCKET来保存这个变量,使得以后不管是发送还是接受,都可以用这个变量来实现(前面已经说了,socket对象就是一个门户,需要通过它来发送和接收)

 

 

checkResponse :需要通过这个函数来实时的检查状态,比如发送是否成功,并且可以得到相应的错误信息,也便于调试。

验证从服务器返回的前三位代码和传递进来的参数是否一样

                           备注:

                           211 帮助返回系统状态

                           214 帮助信息

                           220 服务准备就绪

                           221 关闭连接

                           235 用户验证成功

                           250 请求操作就绪

                           251 用户不在本地,转寄到其他路径

                           334 等待用户输入验证信息

                           354 开始邮件输入

                           421 服务不可用

                           450 操作未执行,邮箱忙

                           451 操作中止,本地错误

                           452 操作未执行,存储空间不足

                           500 命令不可识别或语言错误

                           501 参数语法错误

                           502 命令不支技

                           503 命令顺序错误

                           504 命令参数不支持

                           550 操作未执行,邮箱不可用

                           551 非本地用户

                           552 中止存储空间不足

                           553 操作未执行,邮箱名不正确

                           554 传输失败

为此,需要从socket接收数据,然后和标准的这些码字,比如554来进行比较,通过比较的结果来决定是否响应正确,确定后面是否传输!具体的函数实际上就是一个recv来实现。而接收的结果,需要存放下来,然后手动比较,就有了

return buf[0] == RecvCode[0] && buf[1] == RecvCode[1]&&buf[2] == RecvCode[2] ? true : false;

The recv function receives data from a connected or bound socket.

 

 

Connect:连接部分其实也是一个很简单的功能,就是要发送一个HELO + 服务器名,但是注意这个之前,需要完成一些操作。首先,就是要建立连接,让它知道我要连接它,用connect函数。向对方主动提出连接请求。其次就是如何发送?当然是通过socket发送,调用send函数来实现了(都是底层的API),但是,由于是第一次发送,所以需要创建socket,所以调用了前面的CreateSocket来创建,(注意,代码中多出用到了判断语句,因为网络中很容易就出现错误,所以需要实时的进行if判断,及时定位错误,否则后面的编码就容易出错而不知道如何编写),然后就是要根据send函数的参数来调用了。对于connect函数,根据API原型,需要SOCKADDR_IN类型的参数,也就是记录了远程主机(服务器)的地址信息的东西。我们可以知道的就只用smtp.sina.com.cn(这里以新浪邮箱为例),所以需要的操作就是,首先获取主机名,得到一个HOSTENT的结构体,调用gethostbyname,这个函数的作用就是传入一个主机名,如上面的smtp.sina.com.cn,它会自动的创建一个HOSTENT结构体,并用相应的主机信息来填充它。当然,这个还不够,需要对于SOCKADDR_IN的其它部分赋值,所以需要其它的操作,对于类变量m_SOCKADDR_IN进行初始化,完成之后,就可以调用connect函数了,如果建立成功…………否则…………

一般的SOCKADDR_IN初始化是:

         m_SOCKADDR_IN.sin_family = AF_INET;

         m_SOCKADDR_IN.sin_addr.S_un.S_addr = *(ULONG *) m_HOSTENT->h_addr_list[0];

         m_SOCKADDR_IN.sin_port = htons(Port);

 

完成上面的一步之后,剩下来的就是发送第一个数据报文HELO来“打个招呼”了,这个很简单,因为是有具体的规定,所以不难得到://向服务器发送"HELO "+服务器名

send(m_SOCKET,strTmp.c_str(),strTmp.length(),0)

注意完成之后可以及时的调用CheckResponse来检测结果!

 

 

 

Validate:验证密码的正确性是一个很重要的步骤。首先发送一个报文请求服务器响应,根据得到的信息,决定服务器是否可以连接,从而下一步操作才有可行性。如果连接失败,那么即使用户名和密码都正确,也会出现问题。如何验证正确性?就是发送账号和密码。但是注意的是,在网络SMTP传输中,需要使用编码来传输,也就是说不是直接用的字面值,所以需要调用编码函数,先编码,在传输。这里引用了外部库ZBase64库来实现编码,解码。先不讨论这部分。发送账号和密码很简单,先调用加密函数编码,然后发送,只是在这个之前,需要调用send(m_SOCKET,"AUTH LOGIN\r\n",strlen("AUTH LOGIN\r\n"),0)来发送一个报文,说是要发送账号密码,注意就相当于加了一个label验证的作用。实际上在后面的发送数据的时候,也是按照它定义的格式,先在前面有label,如Mail From,然后加上发送的内容,注意按照标准的格式来,才能实现稳定传输。

 

 

 

SendData:这部分也许是最复杂的。我先来讨论最简单的,就是发送文本文件,而没有带附件的。其实,发送数据的过程和前面的Connect是一样的(特别是第一个HELO报文的发送),但是,要具体根据RFC文档(SMTP对应)的来决定发送的报文头和正文格式。如果格式不正确,那么,发送也是枉然。(注意发送的数据不需要用Base64来编码),只是,需要用一定的个数,比如:

         strTmp="MAIL FROM:<"+SendFrom+">\r\n";

         if(send(m_SOCKET,strTmp.c_str(),strTmp.length(),0) == SOCKET_ERROR)

         {

                  ReleaseSocket();

                  return false;

         }

来实现而已。

这部分最好的资料就是RFC文档,其中讲到了很多实际的例子和格式要求,其它的内容就是重复上面的代码,很简单。

(当然,这部分很容易出错,造成发送说成功,但是收不到!我开始的时候没QUIT部分报文,所以就出现发送提示成功但是没有接收到的情况!)(待续……)

 

posted on 2010-05-02 15:59 deercoder 阅读(2979) 评论(0)  编辑 收藏 引用 所属分类: MFC程序设计入门

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理