程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 無須SMTP服務器中轉直接發送電子郵件

無須SMTP服務器中轉直接發送電子郵件

編輯:關於VC++

前言

大家一定熟悉Foxmail中的“特快專遞”,它能直接將電子郵件發送到對方的郵件服務器中,而不需要經過SMTP服務器中轉,這樣做有什麼好處?第一:發送速度比較快,不需要等SMTP服務器對郵件進行查毒、派發、驗證;第二:你可以及時掌握郵件是否發送成功的信息。有時我們用Outlook發送一封郵件,到第二天對方都沒收到,可我這邊確實已經發送成功了,只好讓對方多收幾次,到了第三天SMTP服務器回信說“不好意思,你發往XXX的郵件因為XXX原因未能送達……”,原來郵件被打回來了,尤其最近163郵箱非常離譜,我發出去的10封郵件,至少有3封會被無故打回來,說什麼“網絡連接失敗”所以被打回,莫名其妙,可能我是免費郵箱的緣故吧,沒辦法只好再申請多幾個郵箱,我現在已經有“[email protected][email protected][email protected] ……”好多郵箱了,就是為了防止給別人發郵件時被無故退回……扯遠了,不好意思。第三:我們有時需要在程序裡將某些敏感信息發送至公司郵箱,例如:計算注冊碼時我們需要用戶操作我們的軟件將申請注冊的信息發送回我們的售後服務郵箱,由我們的工作人員處理來這些郵件。

大家一定會想用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地址呢?也是域名解析服務器的功勞。

電子郵件地址由兩部分組成,例如:[email protected],這裡的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手工發送郵件的過程:

其中紅色矩形框起來的是服務器回應的數據,綠色矩形框起來的是我手工輸入的數據,這裡發送的郵件內容為“我是手工發送的電子郵件”,郵件被直接發送到[email protected]郵箱中,不需要討厭的SMTP服務器中轉,當然,因為這是手工發送的郵件,其內容未經過任何MIME編碼,這封郵件可以被Foxmail或Outlook收到,但可能被判為垃圾郵件,因為這封郵件連標題都沒有,是無頭蒼蠅,肯定是垃圾,呵呵……關於郵件內容的編碼請參考其他相關資料,我有一本書,名叫《Visual C++ 網絡通信協議分析與應用實現》,這本書有詳細的電子郵件編碼介紹,可以下載電子文檔看看。

當我們知道了SMTP通信的全過程,再編寫一個TCP網絡通信程序處理與SMTP服務器請求就不是難事了。本代碼中的CHwSMTP類已經封裝了整個通信過程,可以發送普通的電子郵件,還可以發送帶附件的電子郵件,配合DNS查找,遠程郵局地址MX查詢便可實現任意郵件直接發送到對方郵箱的功能。

軟件操作界面介紹

程序執行後界面如下:

注意事項

看到這裡是否已經很興奮了,想著要自己寫一個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欺騙方式能否成功,請有高手知道解決這個問題的一定要告訴我啊,我的郵箱是:[email protected],先謝過了!

電子郵件在目前的Internet上被廣泛地使用,為了安全很多郵局服務器做了安全認證等諸多限制,我們要想讓自己的SMTP服務器能向所有的郵局發郵件,恐怕還得做更多的努力。

結束語

知識就是力量,知識共享將具有推動時代進步的力量。希望我能為中國的軟件行業盡一份薄力。

你可以任意修改復制本代碼,但請保留版權信息文字不要修改。

由於水平有限,錯誤再所難免,請知情者原諒並告知,多謝!

本文配套源碼

  1. 上一頁:
  2. 下一頁:
欄目導航
Copyright © 程式師世界 All Rights Reserved