Sender邮件发送器
Mail Sender邮件发送器
班 级:电气14
指导教师:罗建军
目录
需求分析„„„„„„„„„„„„„„„„„„„„„„ 2 (A)开发背景„„„„„„„„„„„„„„„„„„ 2 (B)应用对象„„„„„„„„„„„„„„„„„„ 2 (C)项目目标„„„„„„„„„„„„„„„„„„ 2 (D)运行环境„„„„„„„„„„„„„„„„„„ 2 技术路线„„„„„„„„„„„„„„„„„„„„„„ 2
(A)开发环境„„„„„„„„„„„„„„„„„2 (B)总体设计„„„„„„„„„„„„„„„„„„2 (C)详细设计„„„„„„„„„„„„„„„„„„ 3 总效果图„„„„„„„„„„„„„„„„„„„„„„„„ 30 个人
„„„„„„„„„„„„„„„„„„„„„„ 33
1
一、需求分析
(A)开发背景:现代社会是互联网大发展的时代,网络邮件发展更是迅猛,我们编写一个邮件发送程序,对于以后方便的发送电子邮件有很大的好处,从编写这个程序了解电子邮件的工作原理,有很大的帮助。这里我运用smtp
来发送电子邮件。SMTP(简单邮件传输协议)协议是一种邮件发送协议,目前全世界几乎所有的邮件服务器都支持SMTP协议,可以说没有SMTP协议电子邮件寸步难行。
(B)应用对象: 这个软件对于那些经常发送电子邮件的朋友,有很大的方便,可以大大的提高他们的工作效率。
(C)项目目标:实现带有附件的邮件发送功能,并有地址薄的功能。
(D) 运行环境:此软件可在Windows 2000/XP下正常运行,现行的一般微机都可满足其要求。
二、技术路线
(A)开发环境:
操作系统:Windows XP
开发软件:Visual C++,Microsoft Access (B)总体设计:
SMTP(简单邮件传输协议)协议是一种邮件发送协议,他的目标是可靠、高效的传送邮件,他独立于传送子系统,而且仅要求一条可以保证传送数据单元顺序的通道。SMTP协议的一个重要特点是它能够在传送中接力传送邮件。也就是说,如果该邮件服务器知道目的地址的邮件服务器则直接发送;如果不知道目的邮件服务器,则将这封信件按照某种策略转发给就近的一个邮件服务器。通过这种接力式的传送方式,完成邮件的发送。
SMTP的通信过程:针对用户的邮件请求,在发送SMTP和接收SMTP之间建立一个双向通道。接收SMTP可以使最终的接收者也可以是中间的传送者。SMTP命令由发送SMTP发出,由接收SMTP接收,而应答则反方向传送。过程如下:
用户
发 送 接 收
SMTP SMTP 文 件 文件系统
系 统
因为支持邮件附件,所以整个程序显得有点凌乱,程序中共有12个类。分别对SMTP,MIME,邮件以及编码技术进行封装:
, CSMTPEMailAPP类
2
, CSMTPEMailDlg类
, CAboutDlg类
, CAttachmentsDl类
, CSMTP类
, CMailMessage类
, CMIMEMessage类
, CMIMEContentAgent类
, CTextPlain类
, CAppOctetStrea类
, CMIMECode类
, CBase64类
下面从整体上来讲程序结构理清楚:
CSMTPEMailAPP,CSMTPEMailDlg,CAboutDlg这几个类就不用说了。他们是由Visual C++ 6.0的应用程序向导自动生成的框架代码,分别是应用程序类、主对话框类和“关于”对话框类。
CAttachmentsDlg类也是一个对话框类,他封装了添加/移除邮件附件的对话框。
CSMTP类则是程序中的一个关键类,他对SMTP协议进行了包装。也就是说,它实现了SMTP协议的客户端功能。使用该类的成员函数可以登陆到指定的SMTP服务器,设置特定的用户帐号并最后发送撰写好的邮件。
CMailMessage类和CMIMEMessage类对待发送的邮件进行了包装,其中CMailMessage类是CMIMEMessage类的父类,他封装了格式与RFC822定义的格式兼容的电子邮件。而CMIMEMessage类则对MIME扩展各式的邮件进行了包装。
CMIMEContentAgent类,CTextPlain类,CAppOctetStrea类这三个类对MIME邮件格式进行了必要的包装。其中CMIMEContentAgent类是父类,CTextPlain类和CAppOctetStrea类都是从它派生而来的。CTextPlain类封装的是文本格式,CAppOctetStrea类封装的是应用程序文档格式。
CMIMECode类和CBase64类对MIME的编码/解码技术进行了包装。其中CMIMECode是父类,CBase64是子类。
(C)详细设计:
(1).CSMTP类
这个类实现同服务器的交互。利用SMTP协议发送邮件的过程为:首先建立TCP连接,然后使用命令HELO打开通信通道。如:
键入:user:j1010
$telnet 202.117.35.170 25
接收:220 X1 NT-ESMTP Server ctec.xjtu.edu.cn
发送:HELO student@ctec.xjtu.edu.cn
接收:250 hello ctec.xjtu.edu.cn
关闭连接使用命令QUIT,如下:
发送:QUIT
接受:221 Goodbye
3
打开连接通道后,需要进行邮件信息的通信交互,命令介绍如下:
MAIL 初始化邮件传输
mail from:
RCPT 标识单个的邮件接收人;常在MAIL命令后面
可有多个rcpt to:
DATA 在单个或多个RCPT命令后,表示所有的邮件接收人已标识,并初始化
数据传输,以.结束。
VRFY 用于验证指定的用户/邮箱是否存在;由于安全方面的原因,服务器常
禁止此命令
EXPN 验证给定的邮箱列表是否存在,扩充邮箱列表,也常被禁用
HELP 查询服务器支持什么命令
NOOP 无操作,服务器应响应OK
RSET 重置会话,当前传输被取消。
下面是一个例子演示在主机202.117.35.170的student给主机mail.xjtu.edu.cn上的BOB和JANE发送邮件的过程。
发送: MAIL FROM:〈student@ctec.xjtu.edu.cn〉
接收:250 OK
RCPT TO:〈BOB@mail.xjtu.edu.cn〉 发送:
接收: 250 OK
发送:RCPT TO: 〈YONG@mail.xjtu.edu.cn〉
550 No such user here 接收:
发送:RCPT TO:〈JANE@mail.xjtu.edu.cn〉
接收:250 OK
发送:DATA
接收:354 ok, send it; end with
.
发送:Hi, I am in XJTU now,Where are you? //注:这里为邮件征文
发送:How are you?
发送:. //邮件结束标志
接收:250 OK
下面是程序的具体实现过程:
应答代码数组定义如下:
CSMTP::response_code CSMTP::response_table[] =
{
{ 250, _T( "SMTP server error" ) },
{ 220, _T( "SMTP server not available" ) },
{ 354, _T( "SMTP server not ready for data" ) },
{ 221, _T( "SMTP server didn't terminate session" ) }
};
建立同SMTP服务器连接的函数CCMTP::Connect,并发送HELO命令登陆服务器。为了接受服务器的响应,预先分配缓冲区response_buf;调用CSockt类的Creat()函数创建套接字;调用CSockt类的Connect()函数试图连接到SMTP服务器;调用get_response()函数测试是否成功连接到CSMTP服务器;如果连接成功则给服务器发送HELO命令;如果接受到服务器对此指令的回应则表示已经建
4
立起与SMTP服务器的连接,可以进行后续操作了。具体代码如下: //连接SMTP服务器
BOOL CSMTP::Connect()
{
CString sHello;
//本地计算机名称
TCHAR local_host[ 80 ];
if( m_bConnected )
return TRUE;
try
{
//接收缓冲区
response_buf = new TCHAR[ RESPONSE_BUFFER_SIZE ];
if( response_buf == NULL )
{
m_sError = _T( "Not enough memory" );
return FALSE;
}
}
//捕获内存异常
catch( CException *e )
{
response_buf = NULL;
m_sError = _T( "Not enough memory" );
delete e;
return FALSE;
}
//创建Socket
if( !m_wsSMTPServer.Create() )
{
m_sError = _T( "Unable to create the socket." );
delete response_buf;
response_buf = NULL;
return FALSE;
}
//连接服务器
if( !m_wsSMTPServer.Connect( GetServerHostName(), GetPort() ) )
{
m_sError = _T( "Unable to connect to server" );
m_wsSMTPServer.Close();
delete response_buf;
response_buf = NULL;
return FALSE;
5
}
//获取服务器响应
if( !get_response( CONNECT_SUCCESS ) )
{
m_sError = _T( "Server didn't respond." );
m_wsSMTPServer.Close();
delete response_buf;
response_buf = NULL;
return FALSE;
}
gethostname( local_host, 80 );
//发送HELO 命令
sHello.Format( _T( "HELO %s\r\n" ), local_host );
m_wsSMTPServer.Send( (LPCTSTR)sHello, sHello.GetLength() );
//接收服务器响应
if( !get_response( GENERIC_SUCCESS ) )
{
m_wsSMTPServer.Close();
delete response_buf;
response_buf = NULL;
return FALSE;
}
m_bConnected = TRUE;
return TRUE;
}
使用成员函数Connect()连接到SMTP服务器之后,就可以调用SendMessage()韩书法送邮件了;SendMessage()函数分三步完成邮件的发送任务。(1)检查到SMTP服务器的连接是否已经建立。如果还没有,则返回FALSE表示发送失败。 (2)调用FormatMailMessage()函数对待发送的邮件进行格式化。(3)调
transmit_message()函数将邮件安装一定的规则发送个服务器。
BOOL CSMTP::SendMessage(CMailMessage * msg) {
ASSERT( msg != NULL );
if( !m_bConnected )
{
m_sError = _T( "Must be connected" );
return FALSE;
}
if( FormatMailMessage( msg ) == FALSE )
{
return FALSE;
}
if( transmit_message( msg ) == FALSE )
{
6
return FALSE;
}
return TRUE;
}
前面提到的FormatMailMessage()函数定义如下,它主要调用了CMailMessage类的FormatMessage()函数,对于这个函数间后面关于CMailMessage类的具体介绍. BOOL CSMTP::FormatMailMessage( CMailMessage* msg )
{
ASSERT( msg != NULL );
if( msg->GetNumRecipients() == 0 )
{
m_sError = _T( "No Recipients" );
return FALSE;
}
msg->FormatMessage();
return TRUE;
}
邮件的发送是通过CSMTP::transmit_message函数完成的,该函数按照前面介绍的发送过程,依次发送MAIL FROM,RCPT TO, DATA命令完成信件的发送。它发送邮件的次序是这样的:格式化发件人E-Mail地址;格式化收件人E-Mail地址;发送DATA指令;格式化邮件头部;格式化邮件正文;发送作为邮件结束标志的符号。
//发送邮件
BOOL CSMTP::transmit_message(CMailMessage * msg)
{
CString sFrom;
CString sTo;
CString sTemp;
CString sEmail;
ASSERT( msg != NULL );
if( !m_bConnected )
{
m_sError = _T( "Must be connected" );
return FALSE;
}
//发送 MAIL FROM:命令
sFrom.Format( _T( "MAIL From: <%s>\r\n" ), (LPCTSTR)msg->m_sFrom );
m_wsSMTPServer.Send( (LPCTSTR)sFrom, sFrom.GetLength() );
if( !get_response( GENERIC_SUCCESS ) )
return FALSE;
//发送RCPT TO:命令
//可以连续发送多次,这样可以给多个人发送邮件
7
for( int i = 0; i < msg->GetNumRecipients(); i++ )
{
msg->GetRecipient( sEmail, sTemp, i );
sTo.Format( _T( "RCPT TO: <%s>\r\n" ), (LPCTSTR)sEmail );
m_wsSMTPServer.Send( (LPCTSTR)sTo, sTo.GetLength() );
get_response( GENERIC_SUCCESS );
}
//发送DATA命令
sTemp = _T( "DATA\r\n" );
m_wsSMTPServer.Send( (LPCTSTR)sTemp, sTemp.GetLength() );
if( !get_response( DATA_SUCCESS ) )
{
return FALSE;
}
//发送信件头
m_wsSMTPServer.Send( (LPCTSTR)msg->m_sHeader, msg->m_sHeader.GetLength() );
//处理邮件正文
sTemp = cook_body( msg );
//发送邮件正文
m_wsSMTPServer.Send( (LPCTSTR)sTemp, sTemp.GetLength() );
//发送邮件数据结束标志 回车.回车
sTemp = _T( "\r\n.\r\n" );
m_wsSMTPServer.Send( (LPCTSTR)sTemp, sTemp.GetLength() );
if( !get_response( GENERIC_SUCCESS ) )
{
return FALSE;
}
return TRUE;
}
接受并验证服务器的响应函数get_response完成。该函数通过Socket读取返回的
信息,分离出相应代码(250等),判断是否发送成功。
BOOL CSMTP::get_response( UINT response_expected ) {
//输入的响应代码是否合理
ASSERT( response_expected >= GENERIC_SUCCESS );
ASSERT( response_expected < LAST_RESPONSE );
CString sResponse;
UINT response;
response_code* pResp;
//接受响应
if( m_wsSMTPServer.Receive( response_buf, RESPONSE_BUFFER_SIZE ) ==
8
SOCKET_ERROR )
{
m_sError = _T( "Socket Error" );
return FALSE;
}
sResponse = response_buf;
//获取前三个字符(250等)
sscanf( (LPCTSTR)sResponse.Left( 3 ), _T( "%d" ), &response );
//获取响应的代码项
pResp = &response_table[ response_expected ];
//检验是否发送成功
if( response != pResp->nResponse )
{
m_sError.Format( _T( "%d:%s" ), response, (LPCTSTR)pResp->sMessage );
return FALSE;
}
return TRUE;
}
CSMTP::cook_body函数的功能是将邮件正文中的〈CRLF〉.〈CRLF〉替换为〈CRLF〉..〈CRLF〉,具体代码如下:
CString CSMTP::cook_body(CMailMessage * msg) {
ASSERT( msg != NULL );
CString sTemp;
CString sCooked = _T( "" );
//需要删除的字符串标志
LPTSTR szBad = _T( "\r\n.\r\n" );
LPTSTR szGood = _T( "\r\n..\r\n" );
int nPos;
int nStart = 0;
int nBadLength = strlen( szBad );
sTemp = msg->m_sBody;
//文件首部发现 该字符串标志
if( sTemp.Left( 3 ) == _T( ".\r\n" ) )
sTemp = _T( "." ) + sTemp;
//将 szBad替换为szGood
while( (nPos = sTemp.Find( szBad )) > -1 )
{
sCooked = sTemp.Mid( nStart, nPos );
sCooked += szGood;
sTemp = sCooked + sTemp.Right( sTemp.GetLength() - (nPos +
nBadLength) );
}
return sTemp;
9
}
CSMTP::Disconnect函数的功能是推出并断开同SMTP服务器的连接。该函数首先向服务器发送QUIT命令,然后关闭Socket。具体代码如下: BOOL CSMTP::Disconnect()
{
BOOL ret;
if( !m_bConnected )
return TRUE;
发送QUIT命令 //
CString sQuit = _T( "QUIT\r\n" );
m_wsSMTPServer.Send( (LPCTSTR)sQuit, sQuit.GetLength() );
//关闭Socket
ret = get_response( QUIT_SUCCESS );
m_wsSMTPServer.Close();
if( response_buf != NULL )
{
delete[] response_buf;
response_buf = NULL;
}
m_bConnected = FALSE;
return ret;
}
(2).CMailMessage类CMIMEMessage类
在电子邮件的发送和接收中,邮件本身就是一个较为复杂的实体。因此,为了结构化的需要,我们有CMailMessage类CMIMEMessage类对待发送的邮件进行了包装,其中CMailMessage类是CMIMEMessage类的父类,他封装了格式与RFC822定义的格式兼容的电子邮件。而CMIMEMessage类则对MIME扩展各式的邮件进行了包装。
下面是对CMailMessage类的定义,其中virtual类型的函数将会被CMIMEMessage类重载。定义如下:
class CMailMessage
{
public:
CMailMessage();
virtual ~CMailMessage();
//接收人的类型,枚举结构
enum RECIPIENTS_TYPE { TO, CC, BCC }; //
//格式化信息,调用了附件编码程序
void FormatMessage();
//获取接收人的数量
int GetNumRecipients(RECIPIENTS_TYPE type = TO /* */);
10
//获取接收人信息
BOOL GetRecipient( CString& sEmailAddress, CString& sFriendlyName, int
nIndex = 0, RECIPIENTS_TYPE type = TO /* */ );
//添加接收人
BOOL AddRecipient( LPCTSTR szEmailAddress, LPCTSTR szFriendlyName =
"", RECIPIENTS_TYPE type = TO /* */ );
//添加多个接收人
BOOL AddMultipleRecipients( LPCTSTR szRecipients = NULL,
RECIPIENTS_TYPE type = TO /* */ );
//获取每行的字符数
UINT GetCharsPerLine();
//设定每行的字符数
void SetCharsPerLine( UINT nCharsPerLine );
//发信人
CString m_sFrom;
//主题
CString m_sSubject;
CString m_sEnvelope;
//发信程序名称
CString m_sMailerName;
//信件头
CString m_sHeader;
//时间戳
CTime m_tDateTime;
//邮件正文
CString m_sBody;
private:
UINT m_nCharsPerLine;
//接收人结构
class CRecipient
{
public:
//接收人地址
CString m_sEmailAddress;
//接收人名称
CString m_sFriendlyName;
};
//TO 链表
CArray m_Recipients;
//CC链表
CArray m_CCRecipients;
//BCC链表
CArray m_BCCRecipients;
11
protected:
//准备邮件头格式
virtual void prepare_header();
//准备邮件正文格式
virtual void prepare_body();
//结束邮件头
virtual void end_header();
//结束正文
virtual void start_header();
virtual void add_header_line( LPCTSTR szHeaderLine ); };
在这个类的声明中,CMailMessage类提供了几个成员变量以保存成员变量以保
存有件的主要参数,其中重要的包括如下:
, m_sForm 保存发件人的E-mail地址
, m_sMailerName 保存发件人的名称
, m_Recipients 保存收件人列表
, m_sSubject: 保存待发送邮件的主题
, m_tDateTime 保存发信日期时间
, m_sHeader 保存邮件头部
, m_sBody 保存邮件正文
在成员函数中,GetNumRecipients(), GetRecipient(), AddRecipient() 以及
AddMultipleRecipient()都和收件人列表有关,他们分别用来获取列表中的人数、
收件人列表以及向列表中添加收件人。
int CMailMessage::GetNumRecipients(RECIPIENTS_TYPE type /* */) {
//*** old line -> return m_Recipients.GetSize();
int number = 0;
switch(type)
{
case TO: number = m_Recipients.GetSize(); break;
case CC: number = m_CCRecipients.GetSize(); break;
case BCC: number = m_BCCRecipients.GetSize(); break;
}
return number;
}
BOOL CMailMessage::GetRecipient(CString & sEmailAddress, CString & sFriendlyName, int nIndex, RECIPIENTS_TYPE type /* */) {
CRecipient to;
if( nIndex < 0 || nIndex > m_Recipients.GetUpperBound() )
return FALSE;
//*** Begin
12
//*** old line -> to = m_Recipients[ nIndex ];
{
switch(type)
{
case TO: to = m_Recipients[ nIndex ]; break;
case CC: to = m_CCRecipients[ nIndex ]; break;
case BCC: to = m_BCCRecipients[ nIndex ]; break;
}
}
//*** End
sEmailAddress = to.m_sEmailAddress;
sFriendlyName = to.m_sFriendlyName;
return TRUE;
}
BOOL CMailMessage::AddRecipient( LPCTSTR szEmailAddress, LPCTSTR
szFriendlyName)
{
ASSERT( szEmailAddress != NULL );
ASSERT( szFriendlyName != NULL );
CRecipient to;
to.m_sEmailAddress = szEmailAddress;
to.m_sFriendlyName = szFriendlyName;
m_Recipients.Add( to );
return TRUE;
}
//添加多个接收人,szRecepients为接收人字符串,格式为:
//名称1<接收人1地址>;名称2<接收人2地址>;......
BOOL CMailMessage::AddMultipleRecipients(LPCTSTR szRecipients,
RECIPIENTS_TYPE type )
{
TCHAR* buf;
UINT pos;
UINT start;
CString sTemp;
CString sEmail;
CString sFriendly;
UINT length;
int nMark;
int nMark2;
ASSERT( szRecipients != NULL );
length = strlen( szRecipients );
buf = new TCHAR[ length + 1 ];
13
strcpy( buf, szRecipients );
//剥离出接收人
for( pos = 0, start = 0; pos <= length; pos++ )
{
if( buf[ pos ] == ';' ||
buf[ pos ] == 0 )
{
buf[ pos ] = 0;
sTemp = &buf[ start ];
nMark = sTemp.Find( '<' );
if( nMark >= 0 )
{
//分离出名称
sFriendly = sTemp.Left( nMark );
nMark2 = sTemp.Find( '>' );
if( nMark2 < nMark )
{
delete[] buf;
return FALSE;
}
nMark2 > -1 ? nMark2 = nMark2 : nMark2 =
sTemp.GetLength() - 1;
//分离出接收地址
sEmail = sTemp.Mid( nMark + 1, nMark2 - (nMark + 1) );
}
else
{
sEmail = sTemp;
sFriendly = _T( "" );
}
//添加接收人
AddRecipient( sEmail, sFriendly, type );
start = pos + 1;
}
}
delete[] buf;
return TRUE;
}
上文提到的关于FormatMessage()函数,这里来做一介绍,这个函数用来对邮件进行格式化处理,他的定义如下:
void CMailMessage::FormatMessage() {
start_header();
14
prepare_header();
end_header();
prepare_body();
}
FormatMessage()函数调用了几个可被其子类重载的虚拟函数,在CmailMessage
,正是类中,这几个虚拟函数的定义非常简单,而在其子类CMIMEMessage中由于这几个虚拟函数的重载才使得CMIMEMessage对原来的邮件格式进行了扩展。在CmailMessag中,这几个函数及其相关函数是这样定义的: void CMailMessage::start_header() {
m_sHeader = _T( "" );
}
void CMailMessage::end_header() {
m_sHeader += _T( "\r\n" );
}
函数prepare_header()的功能是格式化邮件头信息。该函数为邮件添加FROM, TO, CC等邮件头信息。
void CMailMessage::prepare_header() {
CString sTemp;
sTemp = _T( "" );
// From:
sTemp = _T( "From: " ) + m_sFrom;
add_header_line( (LPCTSTR)sTemp );
// To:
sTemp = _T( "To: " );
CString sEmail = _T( "" );
CString sFriendly = _T( "" );
for( int i = 0; i < GetNumRecipients(); i++ )
{
GetRecipient( sEmail, sFriendly, i );
sTemp += ( i > 0 ? _T( "," ) : _T( "" ) );
sTemp += sFriendly;
sTemp += _T( "<" );
sTemp += sEmail;
sTemp += _T( ">" );
}
add_header_line( (LPCTSTR)sTemp );
// Date:
15
m_tDateTime = m_tDateTime.GetCurrentTime();
sTemp = _T( "Date: " );
sTemp += m_tDateTime.Format( "%a, %d %b %y %H:%M:%S %Z" );
add_header_line( (LPCTSTR)sTemp );
// Subject:
sTemp = _T( "Subject: " ) + m_sSubject;
add_header_line( (LPCTSTR)sTemp );
// X-Mailer
sTemp = _T( "X-Mailer: " ) + m_sMailerName;
add_header_line( (LPCTSTR)sTemp ); }
void CMailMessage::prepare_body() {
// Append a CR/LF to body if necessary.
if( m_sBody.Right( 2 ) != _T( "\r\n" ) )
m_sBody += _T( "\r\n" );
}
E-Mail传送过程中都要对附件文件进行编码,因为E-Mail只能传送ASCII码格式的文字信息,ASCII码为7位码。非ASCII码格式的文件在传送中必须经过编码编成相应的ASCII码后进行传输,在接受到后,接受后要进行编码。若不这样,就会在传输过程总出现编码截位的问题,导致接受方出现“乱码”问题。
MIME(Multipurpose Internet Mail Extention)是常用的编码,MIME定义的是一种编码规格,也可以说是一类编码的统称,能过符合MIME标准的编码方式有多种,而只要符合MIME规格便可以顺利传送,在MIME定义下有两种编码方式Base64和QP(Quote-Printable),QP的规则是对中的7位无重复编码,仅将8位数据转成7位,QP编码适用传送非ASCII码的文字内容,例如中文文件。而Base64的规则是将整个文件重新编码成7位,通常用于传送二进制文件。后文详细介绍Base64算法。
类CMIMEMessage继承与CMailMessage,该类主要完成将附件文件添加到邮件信息中的功能,定义如下:
// 继承于CMailMessage
class CMIMEMessage : public CMailMessage {
public:
CMIMEMessage();
virtual ~CMIMEMessage();
// MIME Type Codes
enum eMIMETypeCode
{
16
//文本类型
TEXT_PLAIN = 0,
//二进制数据流
APPLICATION_OCTETSTREAM,
NEXT_FREE_MIME_CODE
};
//编码方式
enum eMIMEEncodingCode
{
_7BIT = 0,
_8BIT,
BINARY,
QUOTED_PRINTABLE,
BASE64,
NEXT_FREE_ENCODING_CODE
};
//添加多媒体信息
BOOL AddMIMEPart( LPCTSTR szContent,
int nContentType = APPLICATION_OCTETSTREAM,
LPCTSTR szParameters = _T( "" ),
int nEncoding = BASE64,
BOOL bPath = TRUE );
protected:
void insert_message_end( CString& sText );
void register_mime_type( CMIMEContentAgent* pMIMEType );
void insert_boundary( CString& sText );
virtual void append_mime_parts();
virtual void prepare_header();
virtual void prepare_body();
CString m_sNoMIMEText;
//信息边界标志
CString m_sPartBoundary;
//MIME type
CString m_sMIMEContentType; private:
//多媒体信息管理
class CMIMEPart
{
public:
//编码类型
int m_nEncoding;
//媒体类型
17
int m_nContentType;
CString m_sParameters;
BOOL m_bPath;
CString m_sContent;
};
媒体链表 //
CList m_MIMEPartList;
class CMIMETypeManager
{
public:
CMIMEContentAgent* GetHandler( int nContentType );
void RegisterMIMEType( CMIMEContentAgent* pMIMEType);
virtual ~CMIMETypeManager();
CMIMETypeManager();
private:
CCriticalSection m_csAccess;
CList < CMIMEContentAgent*, CMIMEContentAgent* >
m_MIMETypeList;
};
static CMIMETypeManager m_MIMETypeManager;
};
该类中绝大多数函数的功能都是进行数据链表的操作,并没有实质性的算法操作。
AddMIMEPart函数的功能是添加附件信息,该函数将需要发送的附件信息(文件路径等)添加到附件链表中,但是不进行编码。具体代码如下:
BOOL CMIMEMessage::AddMIMEPart(LPCTSTR szContent,
int
nContentType,
LPCTSTR
szParameters,
int nEncoding,
BOOL bPath )
{
CMIMEPart part;
part.m_nContentType = nContentType;
part.m_sParameters = szParameters;
part.m_nEncoding = nEncoding;
part.m_bPath = bPath;
part.m_sContent = szContent;
part.m_sContent.TrimLeft();
part.m_sContent.TrimRight();
if( nContentType == TEXT_PLAIN )
18
m_MIMEPartList.AddHead( part );
else
m_MIMEPartList.AddTail( part );
return TRUE;
}
append_mime_parts()函数的功能是调用编码函数进行编码操作,该函数被
prepare_body()这两个函数将被prepare_body()调用,而prepare_header(),
CMailMessage::FormatMessage()函数调用。
void CMIMEMessage::prepare_header() {
CString sTemp;
// Let the base class add its headers
CMailMessage::prepare_header();
add_header_line( _T( "MIME-Version: 1.0" ) );
sTemp.Format( _T( "Content-Type: %s; boundary=%s" ),
(LPCTSTR)m_sMIMEContentType,
(LPCTSTR)m_sPartBoundary );
add_header_line( (LPCTSTR)sTemp ); }
void CMIMEMessage::prepare_body()
{
if( m_sBody != _T( "" ) )
AddMIMEPart( (LPCTSTR)m_sBody, TEXT_PLAIN, "", _7BIT,
FALSE );
// Initialize the body (replace current contents).
m_sBody = m_sNoMIMEText;
m_sBody += _T( "\r\n\r\n" );
append_mime_parts();
insert_message_end( m_sBody );
CMailMessage::prepare_body();
}
void CMIMEMessage::insert_boundary( CString& sText )
{
CString sTemp;
if( sText.Right( 2 ) != _T( "\r\n" ) )
sText += _T( "\r\n" );
sTemp.Format( _T( "--%s\r\n" ), (LPCTSTR)m_sPartBoundary );
sText += sTemp;
}
void CMIMEMessage::insert_message_end( CString& sText )
{
19
CString sTemp;
if( sText.Right( 2 ) != _T( "\r\n" ) )
sText += _T( "\r\n" );
sTemp.Format( _T( "--%s--\r\n" ), (LPCTSTR)m_sPartBoundary );
sText += sTemp;
}
void CMIMEMessage::register_mime_type(CMIMEContentAgent* pMIMEType)
{
ASSERT( pMIMEType != NULL );
if( pMIMEType == NULL )
return;
m_MIMETypeManager.RegisterMIMEType( pMIMEType ); }
CMIMEMessage::append_mime_parts函数的功能是调用编码函数进行邮件编码
操作,该函数被CMIMEMessag::prepare_body调用,而后者又被FormatMessage调用。具体实现如下:
void CMIMEMessage::append_mime_parts() {
POSITION part_position;
CMIMEPart* pMIMEPart = NULL;
//CMIMEContentAgent为一父类,真正的处理过程
//在其子类CAppOctetStream完成
CMIMEContentAgent* pMIMEType = NULL;
part_position = m_MIMEPartList.GetHeadPosition();
while( part_position != NULL )
{
//获取MIME类型和数据信息
pMIMEPart = & m_MIMEPartList.GetNext( part_position );
pMIMEType =
m_MIMETypeManager.GetHandler( pMIMEPart->m_nContentType );
if( pMIMEType != NULL )
{
//添加边界标志
insert_boundary( m_sBody );
pMIMEType->AppendPart( pMIMEPart->m_sContent,
pMIMEPart->m_sParameters,
pMIMEPart->m_nEncoding,
pMIMEPart->m_bPath,
m_sBody );
}
}
}
20
(3)CMIMEContentAgent, CTextPlain和CAppOctectStream类
上文我们提到MIME邮件格式,MIME支持多种邮件格式,对于这些邮件格式本身,就需要一个封装。这就是由CMIMEContentAgent, CAppOctectStream和CTextPlain类这三个类来完成的。这三个类中,CMIMEContentAgent是父类,其它两个类都是从它派生来的。封装的是应用程序文档格式,CTextPlain类封装的是文本格式。
在父类CMIMEContentAgent中,QueryType()成员函数被用来询问他所支持的MIME格式,当然,该函数也被CTextPlain和CAppOctectStream也继承。此外CMIMEContentAgent还提供了两个纯粹的虚拟函数(即没有给出实现,且必须被其子类重载),一个是GetContentTypeString(),用来返回描述所支持的MIME格式字符串,另一个是AppendPart(),用来在邮件中附加指定格式的特定内容。
CTextPlain和CAppOctectStream对GetContentTypeString()的实现分别如下(它们返回的描述字符串分别是text/plain和application/octet-stream):
CString CTextPlain::GetContentTypeString()
{
CString s;
s = _T( "text/plain" );
return s;
}
CString CAppOctetStream::GetContentTypeString()
{
CString s;
s = _T( "application/octet-stream" );
return s;
}
而对于AppendPart()函数,CTextPlain和CAppOctectStream类采取了完全不同的实现方法。当然,这也是一定的,因为邮件格式的特点正是在这个函数中得到体现的。CTextPlain中的AppendPart()函数首先调用build_sub_header()函数创建子邮件头,然后将要添加的内容用wrap_text()函数进行包装,最后将两部分内容相加即可,如下:
BOOL CTextPlain::AppendPart(LPCTSTR szContent,
LPCTSTR
szParameters,
int nEncoding,
BOOL bPath,
CString &
sDestination)
{
CString sSubHeader;
CString sWrapped;
sSubHeader = build_sub_header( szContent,
szParameters,
21
nEncoding,
bPath );
sWrapped = wrap_text( szContent );
sDestination += (sSubHeader + sWrapped);
return TRUE;
}
build_sub_header()函数和wrap_text()函数如下:
CString CTextPlain::build_sub_header(LPCTSTR szContent,
LPCTSTR
szParameters,
int
nEncoding,
BOOL
bPath)
{
CString sSubHeader;
sSubHeader.Format( _T( "Content-Type: %s%s\r\n" ),
(LPCTSTR)GetContentTypeString(),
szParameters );
sSubHeader += _T( "Content-Transfer-Encoding: " );
switch( nEncoding )
{
// This class handles only 7bit encoding, but others
// may be added here.
default:
//Fall through to...
case CMIMEMessage::_7BIT:
sSubHeader += _T( "7Bit" );
}
sSubHeader += _T( "\r\n\r\n" );
return sSubHeader;
}
CString CTextPlain::wrap_text(LPCTSTR szText) {
CString sTemp;
CString sLeft;
CString sRight;
int lp = 0;
UINT nCount = 0;
int nSpacePos = 0;
ASSERT( szText != NULL );
if( szText == NULL )
22
return sTemp;
sTemp = szText;
while( lp < sTemp.GetLength() )
{
if( sTemp[ lp ] == ' ' )
nSpacePos = lp;
// Reset counter on newline
if( sTemp.Mid( lp, 2 ) == _T( "\r\n" ) )
nCount = 0;
// Wrap text at last found space
if( nCount > m_nWrapPos )
{
sLeft = sTemp.Left( nSpacePos );
sRight = sTemp.Right( sTemp.GetLength() - nSpacePos );
sLeft.TrimRight();
sRight.TrimLeft();
sLeft += _T( "\r\n" );
sTemp = sLeft + sRight;
nCount = 0;
}
else
nCount++;
lp++;
}
return sTemp;
}
但是在CAppOctetStream类的AppendPart()函数中,不但创建子邮件头的函数定义是不一样的,而且获得了子邮件头之后,CAppOctetStream类接下来的操作是调用attach_file()将附件添加到邮件中,这就是说该函数调用attch_file函数来编码附件,调用buile_sub_header()来创建邮件附件信息头,代码如下: BOOL CAppOctetStream::AppendPart(LPCTSTR szContent,
LPCTSTR
szParameters,
int nEncoding,
BOOL bPath,
CString &
sDestination)
{
CStdioFile fAttachment;
ASSERT( szContent != NULL );
if( szContent == NULL )
return FALSE;
23
//打开附件文件
if( !fAttachment.Open( szContent, (CFile::modeRead | CFile::shareDenyWrite |
CFile::typeBinary) ) )
return FALSE;
//创建信息头
sDestination += build_sub_header( szContent,
szParameters,
nEncoding,
TRUE );
//对附件文件进行编码
attach_file( &fAttachment, CMIMEMessage::BASE64, sDestination );
fAttachment.Close();
return TRUE;
}
CString CAppOctetStream::build_sub_header(LPCTSTR szContent,
LPCTSTR szParameters,
int
nEncoding,
BOOL bPath)
{
CString sSubHeader;
CString sTemp;
TCHAR szFName[ _MAX_FNAME ];
TCHAR szExt[ _MAX_EXT ];
//分离出文件目录
_tsplitpath( szContent, NULL, NULL, szFName, szExt );
if( bPath )
sTemp.Format( "; file=%s%s", szFName, szExt );
else
sTemp = _T( "" );
//创建Content-Type信息头
sSubHeader.Format( _T( "Content-Type: %s%s\r\n" ),
(LPCTSTR)GetContentTypeString(),
(LPCTSTR)sTemp );
//表明编码方式为Base64
sSubHeader += _T( "Content-Transfer-Encoding: base64\r\n" );
sTemp.Format( _T( "Content-Disposition: attachment; filename=%s%s\r\n" ),
szFName, szExt );
sSubHeader += sTemp;
//表明信息头结束
24
sSubHeader += _T( "\r\n" );
return sSubHeader;
}
// 读取附件文件进行编码
//pFileAtt:文件指针
//nEncoding:编码类型
//sDestination:目标字符串
void CAppOctetStream::attach_file(CStdioFile* pFileAtt,
int
nEncoding,
CString &
sDestination)
{
//编码类
CMIMECode* pEncoder;
int nBytesRead;
TCHAR szBuffer[ BYTES_TO_READ + 1 ];
ASSERT( pFileAtt != NULL );
if( pFileAtt == NULL )
return;
switch( nEncoding )
{
default:
case CMIMEMessage::BASE64:
try
{
pEncoder = new CBase64;
}
catch( CMemoryException* e )
{
delete e;
return;
}
}
if( pEncoder == NULL )
return;
do
{
try
{
//读取一行文件数据进行编码
nBytesRead = pFileAtt->Read( szBuffer, BYTES_TO_READ );
}
25
catch( CFileException* e )
{
delete e;
break;
}
szBuffer[ nBytesRead ] = 0;
//进行base64编码
sDestination += pEncoder->Encode( szBuffer, nBytesRead );
sDestination += _T( "\r\n" );
} while( nBytesRead == BYTES_TO_READ );
sDestination += _T( "\r\n" );
delete pEncoder;
}
(4)CBase64类和CMIMECode类
Base64是MIME邮件中常用的编码方式之一。它的主要思想是将输入的字符串或数据编码成只含有{'A'-'Z', 'a'-'z', '0'-'9', '+', '/'}这64个可打印字符的串,故称为“Base64”。 Base64编码的方法是,将输入数据流每次取6 bit,用此6 bit的值(0-63)作为索引去查表,输出相应字符。这样,每3个字节将编码为4个字符(3×8 ? 4×6);不满4个字符的以'='填充。
CBase64类实现了对于数据流的编码和解码算法,类的定义如下: class CBase64 : public CMIMECode {
public:
CBase64();
virtual ~CBase64();
//编码、解码函数
virtual int Decode( LPCTSTR szDecoding, LPTSTR szOutput );
virtual CString Encode( LPCTSTR szEncoding, int nSize );
protected:
//编码
void write_bits( UINT nBits, int nNumBts, LPTSTR szOutput, int& lp );
//解码
UINT read_bits( int nNumBits, int* pBitsRead, int& lp );
//输入的字符串长度
int m_nInputSize;
//剩余的字节数
int m_nBitsRemaining;
ULONG m_lBitStorage;
//输入缓冲区
LPCTSTR m_szInput;
26
//掩码
static int m_nMask[];
//转换后的字符表
static CString m_sBase64Alphabet; private:
};
Encode()函数是用来对附件进行编码的,具体代码如下:
//编码函数
CString CBase64::Encode(LPCTSTR szEncoding, int nSize)
{
CString sOutput = _T( "" );
int nNumBits = 6;
UINT nDigit;
int lp = 0;
ASSERT( szEncoding != NULL );
if( szEncoding == NULL )
return sOutput;
m_szInput = szEncoding;
m_nInputSize = nSize;
m_nBitsRemaining = 0;
nDigit = read_bits( nNumBits, &nNumBits, lp );
while( nNumBits > 0 )
{
//获取映射后的编码字符
sOutput += m_sBase64Alphabet[ (int)nDigit ];
nDigit = read_bits( nNumBits, &nNumBits, lp );
}
//如果长度不是4的倍数
//需要用=补足
while( sOutput.GetLength() % 4 != 0 )
{
sOutput += '=';
}
return sOutput;
}
Encode()函数调用进行read_bits()函数bit流的处理工作。read_bits()函数的代码实现如下:
//nNumBits:打算读取的字节数
//pBitsRead:已经读取得字节数
//lp总共读取的字节数
UINT CBase64::read_bits(int nNumBits, int * pBitsRead, int& lp)
{
27
ULONG lScratch;
while( ( m_nBitsRemaining < nNumBits ) &&
( lp < m_nInputSize ) )
{
//从输入的字符串中获取一个
//8bits数据
int c = m_szInput[ lp++ ];
//右移8位
m_lBitStorage <<= 8;
//获取的数据
m_lBitStorage |= (c & 0xff);
m_nBitsRemaining += 8;
}
//根据读取得字节数
//取出不同的6位数据,通过掩码进行映射
if( m_nBitsRemaining < nNumBits )
{
lScratch = m_lBitStorage << ( nNumBits - m_nBitsRemaining );
*pBitsRead = m_nBitsRemaining;
m_nBitsRemaining = 0;
}
else
{
lScratch = m_lBitStorage >> ( m_nBitsRemaining - nNumBits );
*pBitsRead = nNumBits;
m_nBitsRemaining -= nNumBits;
}
return (UINT)lScratch & m_nMask[nNumBits];
}
解码函数为Decode()。解码就是编码的逆过程,具体代码如下: int CBase64::Decode(LPCTSTR szDecoding, LPTSTR szOutput)
{
CString sInput;
int c, lp =0;
int nDigit;
int nDecode[ 256 ];
ASSERT( szDecoding != NULL );
ASSERT( szOutput != NULL );
if( szOutput == NULL )
return 0;
if( szDecoding == NULL )
return 0;
sInput = szDecoding;
28
if( sInput.GetLength() == 0 )
return 0;
//创建解码表
for( int i = 0; i < 256; i++ )
nDecode[i] = -2; // Illegal digit
for( i=0; i < 64; i++ )
{
nDecode[ m_sBase64Alphabet[ i ] ] = i;
//忽略第8位
nDecode[ m_sBase64Alphabet[ i ] | 0x80 ] = i;
//忽略附加的'='
nDecode[ '=' ] = -1;
nDecode[ '=' | 0x80 ] = -1;
}
//初始化输出缓冲区
memset( szOutput, 0, sInput.GetLength() + 1 );
//循环将编码的字符串转换为原来的6bits数据
for( lp = 0, i = 0; lp < sInput.GetLength(); lp++ )
{
c = sInput[ lp ];
nDigit = nDecode[ c & 0x7F ];
if( nDigit < -1 )
{
return 0;
}
else if( nDigit >= 0 )
write_bits( nDigit & 0x3F, 6, szOutput, i );
}
return i;
}
void CBase64::write_bits(UINT nBits,
int nNumBits,
LPTSTR szOutput,
int& i)
{
UINT nScratch;
m_lBitStorage = (m_lBitStorage << nNumBits) | nBits;
m_nBitsRemaining += nNumBits;
while( m_nBitsRemaining > 7 )
{
nScratch = m_lBitStorage >> (m_nBitsRemaining - 8);
szOutput[ i++ ] = nScratch & 0xFF;
m_nBitsRemaining -= 8;
29
}
}
(三)总效果图
基本界面
短信功能
30
发送界面
添加附件
31
地址薄
(四)个人小结
通过这次网络创新实验及一部加深了对于网络编程的理解,扩展了学习的范围,例如:我们用到了基本的控件制作,网络编程,注册表,数据库等知识。也用到了编码方式,对于以后学习密码学很有帮助。在这里总结了以下几个方面:
1. 当进行一个任务或者计划时一定要有统筹安排,这样才能事半功倍。
2. 过程进行当中,一定要循序渐进,这样在整体上才能达到所预期的效果。
3. 任何事情的成功都难免遇到挫折困难,这时不应气馁,要有耐心,决心,
信心,克服艰难险阻,去实现目标。
4. 在过程中,充分利用网络资源,网络上有很多有用的资料供我们来学习,
通过网络来学习是大势所趋。
32
5. 要提高工作效率,不要做多余的无用功,这样是浪费时间,与己与人,
都是没有好处的,要用短时间做多的事情。
6. 阳光总在风雨后,当看到努力的成果时,那份成功的喜悦是不言而喻的。 通过大作业的学习,增加了我对软件应用的兴趣,知道自己的知识还很少,要不断学习去增加自己的知识。知识的海洋无穷无尽,只有那有力的舵手才能在这片海洋上任意遨游。
在最后要感谢罗建军老师提供的这次很好的机会和好的条件、无私的指导,感谢电气14张亮同学帮我提供的一些技术支持,感谢很多帮助我测试软件,给我软件编制过程中提出建议的朋友、同学。
33
34