无须 SMTP 服务器中转直接将电子邮件发送到对方邮箱
前言
大家一定熟悉Foxmail中的"特快专递",它能直接将电子邮件发送到对方的邮件服务器中,而不需要经过SMTP服务器中转,这样做有什么好处?第一:发送速度比较快,不需要等SMTP服务器对邮件进行查毒、派发、验证;第二:你可以及时掌握邮件是否发送成功的信息。有时我们用Outlook发送一封邮件,到第二天对方都没收到,可我这边确实已经发送成功了,只好让对方多收几次,到了第三天SMTP服务器回信说"不好意思,你发往XXX的邮件因为XXX原因未能送达……",原来邮件被打回来了,尤其最近163邮箱非常离谱,我发出去的10封邮件,至少有3封会被无故打回来,说什么"网络连接失败"所以被打回,莫名其妙,可能我是免费邮箱的缘故吧,没办法只好再申请多几个邮箱,我现在已经有"chrys@21cn.com、chrys.xie@gmail.com、hwxie@ust.hk ……"好多邮箱了,就是为了防止给别人发邮件时被无故退回……撤远了,不好意思。第三:我们有时需要在程序里将某些敏感信息发送至公司邮箱,例如:计算注册码时我们需要用户操作我们的软件将申请注册的信息发送回我们的售后服务邮箱,由我们的工作人员处理来这些邮件。
大家一定会想用SMTP(Simple Mail Transfer Protocol)借助SMTP服务器也能通过程序实现邮件发送,但是有一个很大问题就是安全问题,很多著名的邮件服务器运营商对于用软件方式通过SMTP协议频繁提交邮件转发的申请是不欢迎的,我的163邮箱就曾经深受其害,我那次是在写SMTP客户端发送邮件的程序,顺手就用了163的SMTP服务器,我刚发到第5封邮件时就发送失败了,我再登录163网站一查,原来我的账号被封了,原因就是我用软件发送邮件太多了(天啦,才5封而已啊),后来我花了近两个月时间跟新浪公司又赔礼又道歉,还把身份证传真过去了我的账号才被恢复。
剖析邮件传送过程
废话说太多请别介意,现在言归正传,要直接将邮件送到对方(POP或IMAP)服务器上,而不经过SMTP邮件服务器转交,其实也不难,你只要改用Unix/Linux操作系统,直接SendMail命令就能完成,但在Windows下想要实现这个功能恐怕得花一点心思了。我们首先要从协议 RFC821 - Simple Mail Transfer Protocol 入手来分析。
首先我们看一下Email的递送过程:
邮件原文 → 编码 → SMTP客户端 → SMTP转交服务器 → 远程SMTP服务器(对方邮局)。
"特快专递"的实现思路
邮件编码后被递送到一个SMTP转交服务器上,该服务器对信件分检(到同一邮局的被放在一起)后,根据优先级以及信件的先后次序被发送到远程邮局的SMTP服务器上。换句话说,只要我们知道了SMTP转交服务器是如何确定远程邮局SMTP服务器的地址的,就可以直接递送到远程邮局服务器。SMTP转交服务器又是知道远程邮局的地址呢?这就是域名解析所完成的工作了,就好比我们在IE浏览器输入" www.viction.net "这个域名,IE浏览器又如何知道目标服务器的IP地址呢?也是域名解析服务器的功劳。
电子邮件地址由两部分组成,例如: chrys@163.com ,这里的chrys是邮箱名(即用户名,一个用户对应一个邮箱),163.com是邮箱服务器地址,邮箱名和邮箱服务器地址之间以"@"作为分隔。
我们只要向域名服务器发送查询"163.com"的远程邮局服务器地址便可找到远程邮局SMTP服务器的IP 地址,该查询指令被称作MX(Mail Exchange)邮件交换服务器的地址查询。远程邮局SMTP服务器的地址可能不止一个,这时,你可根据信件优先级的不同来选择对应的远程邮局,我为了安全起见会对每一个远程邮局服务器按照等级高低逐一尝试,只要将邮件成功地发送到其中一个邮局我们的任务就完成了。
我们要完成几项编程工作:本机DNS的获取、与DNS服务器通信实现MX指令查询、SMTP邮件提交,下面我们一一阐述。
获取本机 DNS
代码中我封装了一个类CnetAdapterInfo,该类可以获取本机网卡的系列信息,包括本机IP地址、子网掩码、DNS、Wins、网卡MAC地址等相关信息。
首先我们需要调用IPHelpAPI 库中的GetAdaptersInfo()函数来获取系统中所有网卡信息。
DWORD GetAdaptersInfo (
__out PIP_ADAPTER_INFO pAdapterInfo,
__inout PULONG pOutBufLen
);
该函数有两个参数,pAdapterInfo是一个指针,指向一个用户定义的结构体,一般是用HeapAlloc()申请的内存空间,pOutBufLen传入pAdapterInfo所指空间的大小,传出实际需要的缓冲大小,第一次调用该函数时pOutBufLen传入0,函数将返回 ERROR_BUFFER_OVERFLOW 表示需要更多的缓冲,并将实际需要的缓冲长度返回,我们根据实际长度用HeapAlloc()函数申请空间再次调用该函数,以下代码是枚举所有网卡并将信息保存到数组 m_Ary_NetAdapterInfo 中:
#define MALLOC( bytes ) ::HeapAlloc( ::GetProcessHeap(), HEAP_ZERO_MEMORY, (bytes) )
#define FREE( ptr ) if( ptr ) ::HeapFree( ::GetProcessHeap(), 0, ptr )
#define REMALLOC( ptr, bytes ) ::HeapReAlloc( ::GetProcessHeap(), HEAP_ZERO_MEMORY, ptr, bytes )
//
// 枚举网络适配器
// return : ------------------------------------------------------------
// -1 - 失败
// >=0 - 网络适配器数量
//
int CNetAdapterInfo::EnumNetworkAdapters ()
{
DeleteAllNetAdapterInfo ();
~
IP_ADAPTER_INFO* pAdptInfo = NULL;
IP_ADAPTER_INFO* pNextAd = NULL;
ULONG ulLen = 0;
int nCnt = 0;
DWORD dwError = :: GetAdaptersInfo ( pAdptInfo, &ulLen );
if( dwError != ERROR_BUFFER_OVERFLOW ) return -1;
pAdptInfo = ( IP_ADAPTER_INFO* )MALLOC ( ulLen );
dwError = :: GetAdaptersInfo ( pAdptInfo, &ulLen );
if ( dwError != ERROR_SUCCESS ) return -1;
pNextAd = pAdptInfo;
while( pNextAd )
{
COneNetAdapterInfo *pOneNetAdapterInfo = new COneNetAdapterInfo ( pNextAd );
if ( pOneNetAdapterInfo )
{
m_Ary_NetAdapterInfo.Add ( pOneNetAdapterInfo );
}
nCnt ++;
pNextAd = pNextAd->Next;
}
// free any memory we allocated from the heap before
// exit. we wouldn't wanna leave memory leaks now would we? ;p
FREE( pAdptInfo );
return nCnt;
}
针对每个网卡信息,我们需要调用 GetPerAdapterInfo()函数来获取指定网卡的DNS信息,使用方法和GetAdaptersInfo()类似。以下代码获取网卡基本信息:
//
// 根据传入的 pAdptInfo 信息来获取指定网卡的基本信息
//
BOOL COneNetAdapterInfo::Init ()
{
IP_ADDR_STRING* pNext = NULL;
IP_PER_ADAPTER_INFO* pPerAdapt = NULL;
ULONG ulLen = 0;
DWORD dwErr = ERROR_SUCCESS;
ASSERT ( m_AdptInfo.AddressLength > 0 );
t_IPINFO iphold;
~
// 将变量清空
m_bInitOk = FALSE;
m_csName.Empty ();
m_csDesc.Empty ();
m_CurIPInfo.csIP.Empty ();
m_CurIPInfo.csSubnet.Empty ();
m_Ary_IP.RemoveAll ();
m_Ary_DNS.RemoveAll ();
m_Ary_Gateway.RemoveAll ();
#ifndef _UNICODE
m_csName = m_AdptInfo.AdapterName;
m_csDesc = m_AdptInfo.Description;
#else
USES_CONVERSION;
m_csName = A2W ( m_AdptInfo.AdapterName );
m_csDesc = A2W ( m_AdptInfo.Description );
#endif
// 获取当前正在使用的IP地址
if ( m_AdptInfo.CurrentIpAddress )
{
m_CurIPInfo.csIP = m_AdptInfo.CurrentIpAddress->IpAddress.String;
m_CurIPInfo.csSubnet = m_AdptInfo.CurrentIpAddress->IpMask.String;
}
else
{
m_CurIPInfo.csIP = _T("0.0.0.0");
m_CurIPInfo.csSubnet = _T("0.0.0.0");
}
// 获取本网卡中所有的IP地址
pNext = &( m_AdptInfo.IpAddressList );
while ( pNext )
{
iphold.csIP = pNext->IpAddress.String;
iphold.csSubnet = pNext->IpMask.String;
m_Ary_IP.Add ( iphold );
pNext = pNext->Next;
}
// 获取本网卡中所有的网关信息
pNext = &( m_AdptInfo.GatewayList );
while ( pNext )
{
m_Ary_Gateway.Add ( pNext->IpAddress.String );
pNext = pNext->Next;
}
// 获取本网卡中所有的 DNS
dwErr = ::GetPerAdapterInfo ( m_AdptInfo.Index, pPerAdapt, &ulLen );
if( dwErr == ERROR_BUFFER_OVERFLOW )
{
pPerAdapt = ( IP_PER_ADAPTER_INFO* ) MALLOC( ulLen );
dwErr = ::GetPerAdapterInfo( m_AdptInfo.Index, pPerAdapt, &ulLen );
// if we succeed than we need to drop into our loop
// and fill the dns array will all available IP
// addresses.
if( dwErr == ERROR_SUCCESS )
{
pNext = &( pPerAdapt->DnsServerList );
while( pNext )
{
m_Ary_DNS.Add( pNext->IpAddress.String );
pNext = pNext->Next;
}
m_bInitOk = TRUE;
}
~
// this is done outside the dwErr == ERROR_SUCCES just in case. the macro
// uses NULL pointer checking so it is ok if pPerAdapt was never allocated.
FREE( pPerAdapt );
}
~
return m_bInitOk;
}
至此我们已经获取到系统中所有DNS服务器地址了。
MX 指令查询获取远程邮局地址
与DNS服务器通信其实就是一个简单的UDP网络通信,端口号为53,通信的数据格式如下:
~
所有的DNS消息基本上都是相同的数据结构,但DNS RR是采用了其他的数据结构。
QNAME是一个表示域长度的变量,表示每一节有多少字节,例如: www.sockets.com 将表示为:
~
最后的"Additional"通常包含了查询服务器期望被发送的纪录以减少通信量,例如,回应MX查询时通常在"Additional"中包含'A'纪录。
具体的MX查询过程请参加源代码,以下代码实现了获取本机所有DNS,然后逐一尝试MX查询的方法:
//
// 尝试所有的DNS来查询邮局服务器地址
//
BOOL GetMX (
char *pszQuery, // 要查询的域名
OUT t_Ary_MXHostInfos &Ary_MXHostInfos // 输出 Mail Exchange 主机名
)
{
CNetAdapterInfo m_NetAdapterInfo;
m_NetAdapterInfo.Refresh ();
int nNetAdapterCount = m_NetAdapterInfo.GetNetCardCount();
for ( int i=0; i<nNetAdapterCount; i++ )
{
COneNetAdapterInfo *pOneNetAdapterInfo = m_NetAdapterInfo.Get_OneNetAdapterInfo ( i );
if ( pOneNetAdapterInfo )
{
int nDNSCount = pOneNetAdapterInfo->Get_DNSCount ();
for ( int j=0; j<nDNSCount; j++ )
{
CString csDNS = pOneNetAdapterInfo->Get_DNSAddr ( j );
if ( GetMX ( pszQuery, csDNS.GetBuffer(0), Ary_MXHostInfos ) )
return TRUE;
}
}
}
return FALSE;
}
如果查询"gmail.com"的邮局服务器地址,将得到如下的结果:
gsmtp147.google.com 50
gsmtp183.google.com 50
gmail-smtp-in.l.google.com 5
alt1.gmail-smtp-in.l.google.com 10
alt2.gmail-smtp-in.l.google.com 10
用 SMTP 协议给远程邮局直接发送邮件
SMTP是一个简单邮件传输协议,通过TCP连接服务器的25端口号即可进行数据通信,以下是我用telnet手工发送邮件的过程:
~
其中红色矩形框起来的是服务器回应的数据,绿色矩形框起来的是我手工输入的数据,这里发送的邮件内容为"我是手工发送的电子邮件",邮件被直接发送到 chrys.xie@gmail.com 邮箱中,不需要讨厌的SMTP服务器中转,当然,因为这是手工发送的邮件,其内容未经过任何MIME编码,这封邮件可以被Foxmail或Outlook收到,但可能被判为垃圾邮件,因为这封邮件连标题都没有,是无头苍蝇,肯定是垃圾,呵呵……关于邮件内容的编码请参考其他相关资料,我有一本书,名叫《 Visual C++ 网络通信协议分析与应用实现 》,这本书有详细的电子邮件编码介绍,可以下载电子文档看看。
当我们知道了SMTP通信的全过程,再编写一个TCP网络通信程序处理与SMTP服务器请求就不是难事了。本代码中的CHwSMTP类已经封装了整个通信过程,可以发送普通的电子邮件,还可以发送带附件的电子邮件,配合DNS查找,远程邮局地址MX查询便可实现任意邮件直接发送到对方邮箱的功能。
软件操作界面介绍
程序执行后界面如下:
~
From中可以输入一个虚假的邮箱地址,也可以输入真实的邮箱地址(如:21cn的邮箱),但不能输入163的邮箱,否则发送会失败,163邮箱不太欢迎大家用软件方式进行邮件收发,人家要靠这个吃饭嘛。
注意事项
看到这里是否已经很兴奋了,想着要自己写一个SMTP服务器,甚至想要写一个邮局服务器程序,但我觉得恐怕还没那么容易,我用这个软件给我的google邮箱(gmail)发送邮件,很快就能成功收到了,可我尝试过用这种方式给163邮箱和21cn邮箱发送邮件时却失败了,发给163时服务器说你的IP不被允许,提示信息如下:
550-5.7.1 [116.25.186.155] The IP you're using to send mail is not authorized
550-5.7.1 to send email directly to our servers. Please use the SMTP
看来163邮箱只接收大牌邮件服务器发过来的邮件,难怪我们这些免费的163用户发送邮件时常会被退回,原来163服务器还认牌子的,faint!
到底要怎么样做才可以直接给163等著名的邮局发邮件呢?不知道用IP欺骗方式能否成功,请有高手知道解决这个问题的一定要告诉我啊,我的邮箱是: chrys@163.com ,先谢过了!
电子邮件在目前的Internet上被广泛地使用,为了安全很多邮局服务器做了安全认证等诸多限制,我们要想让自己的SMTP服务器能向所有的邮局发邮件,恐怕还得做更多的努力。
结束语
知识就是力量,知识共享将具有推动时代进步的力量。希望我能为中国的软件行业尽一份薄力。
你可以任意修改复制本代码,但请保留版权信息文字不要修改。
由于水平有限,错误再所难免,请知情者原谅并告知,多谢!
~
源代码下载