ASP.NET 的托管 C++ 电子邮件验证控件






4.19/5 (18投票s)
2002年9月27日
13分钟阅读

262125

2409
使用托管 C++ 实现的 ASP.NET 验证控件,通过连接到指定为域的邮件交换器 (Mail eXchanger) 的 SMTP 服务器来验证电子邮件地址。包含 C++ .NET 与 Win32 API 互操作性的示例。验证不是基于正则表达式的。
引言
本文是我写的第二篇 ASP.NET 验证控件文章,但这次是用托管 C++ 实现的。主要是为了其与 Win32 API 的互操作性。目的是创建一个验证控件,该控件将连接到某个域的 SMTP 服务器,以确定输入的电子邮件地址是否有效。
我将首先提供解决方案的非常高层次的概述,解释它是如何通过连接到 SMTP 服务器并发出命令来工作的。然后,我将提供实现概述——代码的预期工作方式,最后深入到实际代码中进行解释。
解决方案概览
它会获取输入的电子邮件地址并确定域名(在上图中,域名是 webmaster@codeproject.com),以便进行 DNS 查询以检索 MX 记录。MX 代表 Mail Exchange,它包含接受该域电子邮件的所有服务器。通常不止一个,这可以用来分发负载,也提供冗余连接——不同的服务器通常分布在不同的网络中,以确保即使某个连接失败,电子邮件仍能路由。MX 记录还与其关联的优先级,以便 MTA(Mail Transfer Agents - 负责连接服务器传输电子邮件的软件,例如 Sendmail 等)知道首先连接哪个服务器。如果连接失败,它们可以尝试其他服务器。验证控件将连接到第一个服务器并尝试启动正常的 SMTP 连接,如果失败,它将尝试连接到下一个服务器,依此类推。实际上,它会尝试所有记录,直到找到一个可以接受电子邮件的服务器。
一旦连接建立,它将按照 SMTP 协议进行(有关更多详细信息,请参阅 RFC 821)。以下是 RFC 的摘录:
S: MAIL FROM:<Smith@Alpha.ARPA>
R: 250 OK S: RCPT TO:<Jones@Beta.ARPA>
R: 250 OK S: RCPT TO:<Green@Beta.ARPA>
R: 550 No such user here S: RCPT TO:<Brown@Beta.ARPA>
R: 250 OK
通常,这会继续发出更多命令来实际发送消息。连接最初会告知服务器电子邮件来自谁,然后给出 3 个收件人。第一个和最后一个地址被接受。但是,Green@Beta.ARPA 未被接受。这本质上就是验证控件所做的,就像连接到另一个 MTA 一样连接到 SMTP 服务器并尝试向用户发送消息。
如果服务器返回 ok
响应,那么验证控件就可以返回 true,验证成功。
值得注意的是,SMTP 协议确实支持 VRFY
命令,该命令旨在验证用户。然而,由于滥用,SMTP 服务器几乎总是禁用此命令。因此,尝试发送电子邮件是唯一的方法。
实现概述
该解决方案完全用托管 C++ 实现。这是使用 Win32 API 发出 DNS 查询的最简单方法。首先,我将快速概述这些类及其作用。
EmailValidator 类
EmailValidator
类是 BaseValidator
的派生类,可以放置在 ASP.NET 网页表单中。
方法
EvaluateIsValid
从
BaseValidator
重写。在页面验证时调用。应根据关联控件的验证是否成功或失败来返回 true 或 false。ControlPropertiesValid
从
BaseValidator
重写。用于确定关联控件是否有效。该实现使用Control::FindControl
获取控件的引用,并将其存储在私有成员变量_emailAddressBox
中。
属性
LocalServer
在 SMTP 连接期间使用。SMTP 协议规定服务器应通过 HELO 命令进行自我标识。这允许从标记属性设置。
FromEmail
在 SMTP 连接期间使用。此地址用于 MAIL FROM 命令。
Query 类
Query 类实现一个名为 GetMx
的方法,该方法用于检索指定域名的 MX 记录。它接受一个 String* 格式的域名参数,并以包含 MxRecord 结构体并填充的 ArrayList 作为结果返回。
SmtpMailer 类
SmtpMailer
类连接到 SMTP 服务器并发出命令。对于每个命令,都会通过 IsOk
函数检查返回值。如果返回的代码以 1、2 或 3 开头,则可以发出下一个命令。如果发出 RCPT TO
命令并返回错误编号,则认为验证失败。
主方法是 WillAcceptAddress
,它将连接到服务器(通过 String *smtpServer
参数设置),发出命令并进行检查。如果 RCPT TO 命令返回 ok,则 WillAcceptAddress
将返回 true,整体控件验证将成功。如果失败,并且域还有其他列出的 MX 记录服务器,则将按优先级(数字最小的优先)顺序检查剩余的服务器。
MxRecord 结构体
在构建 DNS 查询代码时,我创建了一个小型 C# 控制台应用程序来测试结果,因此传输结果的最简单方法是通过一个结构体 ArrayList,该结构体存储 SMTP 服务器地址和优先级。优先级用于设置应尝试服务器的顺序。通常,域会有备份服务器,以防单个机器发生故障,确保电子邮件不丢失。
该结构体还支持 IComparable
接口,以便在 ArrayList
中根据优先级进行排序。
解决方案实现
EmailValidator 类
ControlPropertiesValid
代码相当自明。它获取对验证器关联控件(应该是 TextBox
)的引用。在获得此引用后,将其转换为私有成员变量(_emailAddressBox
),供以后使用。如果成功,则方法返回 true。如果 ControlToValidate
指向 TextBox
以外的任何内容,则会抛出异常。
// Find the edit box control that contains the email address, and // store it in a private member variable. bool EmailValidator::ControlPropertiesValid() { Control* ctrl = Control::FindControl( this->ControlToValidate ); _emailAddressBox = __try_cast<TextBox*>(ctrl); return true; }
EvaluateIsValid
此方法用于执行验证。首先,它通过正则表达式确定电子邮件地址的域名(本文底部 参考资料 中有更多详细信息)——获取 @ 符号之后的所有内容,因此 webmaster@codeproject.com 将保留 codeproject.com。对我来说,正则表达式看起来很复杂,但在使用 .NET 一段时间后,它们证明非常有用。
确定域名后,就该发出 DNS 查询以检索 MX 记录(以 MxRecord 结构体的形式)并将其存储在 ArrayList
中。一旦返回了 MX 记录列表,我们就连接到每个 SMTP 服务器并发出命令。这将重复进行,直到找到一个接受用户电子邮件的服务器,或者没有剩余的 SMTP 服务器可供尝试。
SMTP 连接由 SmtpMailer
类处理,该类将在稍后介绍。
bool EmailValidator::EvaluateIsValid() { String* domainName; // What's the domain name to look-up? // Do a Regex search to find the domain name part. Regex* r = new Regex(S"^*@(?<domain>\\S+)"); if (r->IsMatch( _emailAddressBox->Text )) domainName = r->Match( _emailAddressBox->Text )-> Result("${domain}")->ToString(); else return false; // Create an ArrayList of MxRecord structures containing // the mail servers to check... ArrayList* serverList = Etier::Dns::Query::GetMx( domainName ); serverList->Sort(); // Create a SmtpMailer instance, and set the default parameters // for the SMTP sessions. SmtpMailer *mail = new SmtpMailer( m_sLocalServer, m_sFromEmail ); // Go through each MxRecord in the serverList to see if the // email address will be accepted by any of them. int i = 0; int nRecordCount = serverList->Count; while ( i < nRecordCount ) { MxRecord mx = *dynamic_cast<__box MxRecord*>(serverList->get_Item(i)); if (mail->WillAcceptAddress( mx.NameExchange, _emailAddressBox->Text )) return true; i++; } return false; }
在了解了验证控件如何执行验证之后,现在来看其他使用的类,即 Query
和 SmtpMailer
。
Query 类
Query
类位于 Etier::Dns
命名空间中,并使用 Win32 API 获取 MX 记录列表。它有一个名为 GetMx
的方法,该方法使用 DnsQuery API 函数。请注意,此函数仅包含在 Windows 2000 及更高版本中。(来自 MSDN 的引用)“像许多 DNS 函数一样,DnsQuery 函数类型以多种形式实现,以促进不同的编码方法”。包含一个 Util
类来实现 ConvertStringToLPCTSTR
,用于在 LPCTSTR
和 System::String
类型之间进行转换。其实现不是解决问题的关键,所以我不会在本文章中详细介绍,但这是在 Microsoft 公共新闻组中搜索到的。
调用 DnsQuery
函数,传递(除其他参数外)域名、查询类型(DNS_TYPE_MX
)以及指向 DNS_RECORD 指针——该指针用于保存查询结果。
GetMx 方法实现
// GetMx method // // Returns an ArrayList of MxRecord structs with // the MX records. static ArrayList* GetMx(String* domainName) { DNS_STATUS status; DNS_RECORD* result = 0; #ifdef _UNICODE status = DnsQuery_W ( Util::ConvertStringToLPCTSTR(domainName), DNS_TYPE_MX, DNS_QUERY_STANDARD, NULL, &result, NULL ); #else status = DnsQuery_A ( Util::ConvertStringToLPCTSTR(domainName), DNS_TYPE_MX, DNS_QUERY_STANDARD, NULL, &result, NULL ); #endif
如果 DnsQuery
函数成功,将创建一个 ArrayList,用于包含 MxRecord
类型。在遍历 DNS_RECORD
结构中包含的链表时,会填充此列表。DNS_RECORD
结构包含 struct _DnsRecord * pNext
——指向下一个记录的指针。通过将当前 DNS_RECORD
设置为 pNext
,可以遍历返回的每个记录。
我最初认为通过发出 DNS_TYPE_MX
查询,结果只会包含 MX 记录。然而,这导致我收到异常,我不得不包含一个条件来检查当前结果的类型(包含在 wType
字段中)是否为 DNS_TYPE_MX
。如果符合,则初始化一个 MxRecord
结构体,其中包含 SMTP 服务器的地址和优先级。
必须使用 String::Copy
来复制通过 pNameExchange
指针获得的字符串,因为当 GetMx 完成时,result
将被删除,从而导致现有引用也无效并抛出异常。
// Create the ArrayList type that will contain // the MxRecord structs ArrayList* aHostList = new ArrayList(); if (SUCCEEDED(status)) { // If the call succeeded, go through the results // picking out the MX records and creating MxRecord // structs to insert into the ArrayList if (result!=0) { // Loop through all the DNS records to find the MX ones. // Check that we've not reached a NULL pointer to the next // record and that the pNext pointer is not the same as the // pointer to the current record. while ( (result->pNext!=NULL) && (result->pNext != result)) { if (DNS_TYPE_MX == result->wType) { MxRecord mx; // create the empty struct to fill it mx.NameExchange = String::Copy(((String*)(LPSTR)result-> Data.MX.pNameExchange) ); mx.nPriority = result->Data.MX.wPreference; aHostList->Add( __box(mx) ); // box the __value struct //and add it to the ArrayList } result = result->pNext; // move to the next DNS record } } // Clean up by freeing up the records DnsRecordListFree(result,DnsFreeRecordList); } return aHostList;
本文的读者可能来自 ASP 背景,主要是 VB 或 C#,可能不了解装箱和拆箱,为了他们的利益,我将简要介绍一下。装箱是将值类型转换为引用类型的过程。ArrayList
的 Add
方法保存指向任何 Object
(或 Object
派生类型)实例的指针。由于 Object
是引用类型,因此有必要将我们的值类型装箱(或包装)到一个引用类型中。由于这涉及在托管堆上创建一个新对象,C++ 需要显式使用 __box
关键字。这可以避免关于它是否仍指向旧实例的任何歧义。
然后可以使用拆箱从引用类型创建值类型。在 EmailValidator
类中,这是通过以下代码实现的:
MxRecord mx = *dynamic_cast<__box MxRecord*>(serverList->get_Item(i));
有必要告诉运行时引用类应如何解释,这通过解引用装箱的 MxRecord
的指针来实现。还必须告诉运行时如何解释引用类型(因此需要 dynamic_cast
)。C# 会自动进行装箱和拆箱,但 C++ 会非常清楚地表明创建了另一个对象,因此通过更改装箱的类型,不会更改原始值类型。装箱和拆箱也是相当密集的 I/O 操作,所以最好尽量少执行。
我决定创建自己的结构体而不是使用 DNS_RECORD
,以便与其他 .NET 应用程序的互操作性尽可能轻松。例如,可以从 C# 使用相同的 DNS 查询代码,而无需触及 Win32 API(这是 .NET,特别是 C++ .NET 的一大优点)。事实上,在开发过程中,我制作了一个快速的 C# 控制台应用程序来测试代码。它还展示了 Visual Studio .NET 的一个最大优点——跨语言调试的能力!
现在是快速看一下 MxRecord
结构体的时候了。
public __value struct MxRecord : System::IComparable { // IComparable::CompareTo int CompareTo( System::Object *obj ) { // Unbox the object to the MxRecord struct MxRecord mx = *dynamic_cast<__box MxRecord*>(obj); // return the difference between the two priorities return this->nPriority - mx.nPriority; } String *NameExchange; // holds the address for the exchanger int nPriority; // priority index };
该结构体实现了 IComparable
接口的 CompareTo
方法。由于记录将被放入 ArrayList
中,因此能够根据其优先级对它们进行排序会很好。为此,有必要包括对 IComparable
接口的支持。CompareTo
方法将当前实例与同类型的另一个对象进行比较,如果实例小于 *obj,则应返回 < 0;如果相等,则返回 0;如果大于,则返回 > 0。
回到整体解决方案,我们现在有一个用 MxRecord
结构体填充的 ArrayList
,现在是时候开始连接它们以查看它们是否接受用户的电子邮件了。所有 SMTP 会话都由 SmtpMailer
类处理,我们接下来将对此进行介绍。
SmtpMailer 类
SmtpMailer
类基于 Albert Pascual 在他的 CodeProject 文章“Sending mail in Managed C++ using SMTP”中的代码。
首先,构造函数
SmtpMailer::SmtpMailer( String *localServer, String *fromEmail ) { m_sLocalServer = localServer; m_sFromEmail = fromEmail; }
SmtpMailer
实例在连接过程中存储本地服务器和发件人电子邮件地址的详细信息,这些信息通过构造函数设置。
WillAcceptAddress
此方法实际连接到指定的服务器,并按照 SMTP 协议发出命令。代码很大程度上是自明的,对于任何没有 C++ 经验的人来说都应该易于理解。
它使用 .NET Framework 的 TcpClient
连接到服务器,并通过服务器的 NetworkStream
发出命令。同样,对我来说,流是我在 .NET 之前从未用过的东西,它们非常灵活。
在发出每个 SMTP 命令后,会检查其结果(通过 RdStrm->ReadLine
调用获得),以确定服务器是否有效返回了ok或error。Ok 值通过检查返回字符串的第一个数字来确定,如果介于 1 和 3 之间,则服务器接受了命令并准备接受下一个。如果返回错误值,则 WillAcceptAddress
将返回 false,并且任何连接都将被关闭。此检查通过 IsOk
方法(该方法又使用正则表达式)进行。
重要的 SMTP 命令是 RCPT TO。此命令用于告知 MTA 任何收件人,此命令的结果用于确定验证是否应该成功。
// WillAcceptAddress opens an SMTP connection, and then issues a number of // commands to determine whether the server will accept email for the given // email address. bool SmtpMailer::WillAcceptAddress( String *smtpServer, String *emailAddress ) { NetworkStream *pNsEmail; StreamReader *RdStrm; String *Data; bool bIsValid = false; unsigned char sendbytes __gc[]; TcpClient *pServer = new TcpClient(smtpServer,25); pNsEmail = pServer->GetStream(); RdStrm = new StreamReader(pServer->GetStream()); if (!IsOk( RdStrm->ReadLine() )) // Was the server reply ok? return false; Data = String::Format("HELO {0}\r\n", m_sLocalServer); sendbytes = System::Text::Encoding::ASCII->GetBytes(Data); pNsEmail->Write(sendbytes, 0, sendbytes->get_Length()); sendbytes = 0; Data = 0; if (!IsOk( RdStrm->ReadLine() )) return false; Data = String::Format("MAIL FROM:<{0}>\r\n", m_sFromEmail); sendbytes = System::Text::Encoding::ASCII->GetBytes(Data); pNsEmail->Write(sendbytes, 0, sendbytes->get_Length()); sendbytes = 0; Data = 0; if (!IsOk( RdStrm->ReadLine() )) return false; Data = String::Format("RCPT TO:<{0}>\r\n",emailAddress); sendbytes = System::Text::Encoding::ASCII->GetBytes(Data); pNsEmail->Write(sendbytes, 0, sendbytes->get_Length()); sendbytes = 0; Data = 0; // Store the return of WillAcceptAddress in the bIsValid flag // thus allowing us to close connections and clean up before returning // from the function. bIsValid = IsOk( RdStrm->ReadLine() ); Data = "QUIT\r\n"; sendbytes = System::Text::Encoding::ASCII->GetBytes(Data); pNsEmail->Write(sendbytes, 0, sendbytes->get_Length()); sendbytes = 0; Data = 0; pNsEmail->Close(); RdStrm->Close(); pServer->Close(); return bIsValid; }
WillAcceptAddress
为找到的每个 MX 记录调用。例如,对 codeproject.com 执行 DNS 查询会产生一个结果——mail.codeproject.com,因此只进行一次连接。如果该服务器在会话期间返回错误代码,WillAcceptAddress
将失败,并且由于只有一个服务器,因此整个验证将失败。如果存在更多服务器,它们也将被检查。
示例用法
下载内容包含一个使用该程序集的 ASP.NET Web 应用程序。但是,这是您将用于在页面上放置标记的代码。
首先,有必要将程序集中的命名空间映射到页面,以便可以引用任何控件。操作方法如下:
<%@ Register TagPrefix="etier" Namespace="Etier" Assembly="SmtpSend" %>
然后可以按如下方式包含验证器:
<etier:EmailValidator Id="MyValidator" Display="none" ControlToValidate="Address" ErrorMessage="* Invalid" RunAt="server" EnableClientScript="False" LocalServer="oobaloo.co.uk" FromEmail="webmaster@oobaloo.co.uk" />
代码基本上与任何验证器相同,只是添加了 FromEmail
和 LocalServer
属性。
结论
再次让我赞叹 ASP.NET :) 页面验证只是其使 Web 应用程序开发真正令人愉悦的众多功能之一。我以前做过很多 MFC 开发,能够使用 C++ 来创建 ASP.NET 控件(即使托管扩展语法使代码看起来有点笨拙)真是太棒了。
希望这对于 C++ 世界的人们来说很有用,让他们知道仍然可以使用 C++ 和 ASP.NET。确实,ASP.NET 不包含 C++ 编译器,因此直接在 ASPX 文件或作为代码隐藏部分编写 C++ 代码是行不通的。然而,可以通过创建程序集并进行预编译来生成代码。
另一个目标是向那些 C++ 经验很少或没有的人(因为假定大多数 ASP 开发人员都有很强的 VB 背景)展示它为什么如此出色。C++ .NET 在其他 .NET 语言中独树一帜,因为它能够生成非托管或托管代码(实例存储如何维护),并且还具有强大的互操作性支持——这是将此解决方案实现为托管 C++ 而不是 C# 的主要原因之一。使用 MC++,可以通过包含任何必要的头文件(DnsQuery
来自 windns.h
)直接调用函数。如果要在 C# 中使用 PInvoke 来完成此操作,我将不得不包含大量的 DllImport
语句,以及定义任何结构体和方法,这很快就会变得非常乏味。
代码可以免费供任何人使用和改进,我绝不是 C++ 大师,所以我确定有些部分可以更有效地实现,或者设计得更好。如果您对代码进行了改进(或发现明显的错误),我很乐意收到您的来信。
参考文献
正则表达式
.NET Framework 正则表达式(MSDN)
历史
- 2002/12/15 - 感谢 Timothy Glenn Stockstill 的帖子,我已更新代码以包含对
DnsRecordListFree
的调用。抱歉花了我这么长时间才更新代码和存档。