A Managed C++ Email Validator Control for ASP.NET






4.19/5 (18 votes)
Sep 27, 2002
13 min read

260071

2409
An ASP.NET Validator Control implemented in Managed C++ that can be used to verify email addresses by connecting to SMTP servers listed as Mail eXchangers for a domain, includes example of Win32 API Interoperability with C++ .NET. The validation is not RegEx based.
Introduction
This article is my second ASP.NET Validator Control article, but this time implemented in Managed C++. This was primarily for its interoperability with the Win32 API. The aim is to produce a validator control that will connect to SMTP servers for a domain to establish whether the entered email address is valid.
I will first provide a very high level overview of the solution, explaining how it works by connecting to SMTP servers and issuing commands. After that I will then provide implementation overviews - how the code is intended to work before finally going into the actual code and explaining how it works.
Solution Overview
It takes the entered email address and determines the domain (in the picture above, the domain being webmaster@codeproject.com), this is so that a DNS query can be made to retrieve MX records. MX stands for Mail Exchange and they hold any servers which will accept email for the domain. Quite often there is more than one, this can be used to distribute load but also to provide redundant connections - different servers are often distributed across networks to ensure that should any connection fail email can still be routed. MX records also have a priority associated with them, so that MTA's (Mail Transfer Agents - the bits of software that connect to servers shifting email around, e.g. Sendmail etc.) know which server to connect to first. If the connection fails they can then try others. The validator control will connect to the first server and try to start a normal SMTP connection, if that fails it will then try to connect to the next server and so on. In fact, it will try all records until it reaches a server that will accept email.
Once the connection has been established it proceeds as per the SMTP protocol (see RFC 821 for more details). Here is an extract from the 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
Normally, this would continue by issuing further commands to actually send a message. The connection initially informs the server who the email is from, and then gives 3 recipients. The first and final addresses are accepted. However, Green@Beta.ARPA is not accepted. This is essentially what the Validator Control does, connect to an SMTP server as if it were another MTA and tries to send a message to a user.
Provided the server returns an ok
response then the validator
control can return true and validation succeeds.
It's worth noting that the SMTP protocl does include support for a VRFY
command
which was intended to verify a user. However, due to misuse this is almost always
turned off on SMTP servers. As a result, attempting to send an email is the
only way.
Implementation Overview
The solution is implemented entirely in Managed C++. This was the easiest way to use the Win32 API to issue the DNS queries. Firstly, I will provide a quick overview of the classes and their roles.
EmailValidator Class
The EmailValidator
class is the BaseValidator
derived
class that can be put into an ASP.NET web form.
Methods
EvaluateIsValid
Overriden from
BaseValidator
. Is called when the page is validated. Should return true or false depending on whether validation for the associated control succeeded or failed.ControlPropertiesValid
Overriden from
BaseValidator
. Used to determine whether the associated control is valid. The implementation usesControl::FindControl
to get a reference to the control, and stores it in the_emailAddressBox
private member variable.
Properties
LocalServer
Used during the SMTP connection. The SMTP protocol dictates that the sever should identify itself as part of the HELO command. This allows it to be set from the tag properties.
FromEmail
Used during the SMTP connection. This address is used in the MAIL FROM command.
Query Class
The Query class implements a single method - GetMx
- which is used to retrieve
MX records for a specified domain name. It takes the domain name in the form
of a single String*
parameter, and returns the results as an ArrayList
populated with MxRecord
structures.
SmtpMailer Class
The SmtpMailer
class connects to the SMTP servers and issues commands.
For every command it issues a return value is checked through the IsOk
function. Provided the code returned starts with 1, 2, or 3 then the next command
can be issued. If the RCPT TO
command is issued and returns
an error number, then the validation is said to have failed.
The main method is WillAcceptAddress
which will connect to a server
(set through the String *smtpServer
parameter), issue commands
and check them. If the RCPT TO command returns ok then WillAcceptAddress
will return true and overall control validation will have succeeded. If it fails
and there are other servers listed as MX records for the domain the remaining
servers will be checked in order of their priority (lowest number first).
MxRecord Structure
Whilst building the DNS querying code I produced a small C# console application
I could use to test results, so the easiest way of transferring the results
was through an ArrayList
of structures that would hold the SMTP
server address and a priority. The priority is used to set the order in which
servers should be tried. Often domains have backup servers in case any single
machine should fail ensuring that emails are not lost.
The structure also includes support for the IComparable
interface
to allow sorting within the ArrayList
based on the priority.
Solution Implementation
EmailValidator Class
ControlPropertiesValid
The code is fairly self explanatory, it obtains a reference to the Validator's
associated control (which ought to be a TextBox
). After obtaining
this reference it is then cast to a private member variable (_emailAddressBox
)
to be used in future. If this succeeds then the method returns true. If ControlToValidate
points to anything other than a TextBox
an exception will be thrown.
// 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
The purpose of this method is to perform the validation. Firstly it determines the domain name of the email address through a Regular Expression (References to further details are at the bottom of the article) - by taking anything after the @ symbol, so webmaster@codeproject.com would keep codeproject.com. To me Regular Expressions look damned complicated but after working with .NET for a while now they're proving extremely useful.
After the domain name has been determined its time to issue the DNS query to
retrieve MX records (in the form of MxRecord structures) that will be stored
in an ArrayList
. Once a list of MX records have been returned we
then connect to each SMTP server and issue commands. This will be repeated until
a server is found that will accept emails for the user, or there are no SMTP
servers left to try.
SMTP Connections are handled by the SmtpMailer
Class which will
be looked at later.
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; }
After seeing how the validator control performs validation, its time to look
at the other classes used, namely the Query
and SmtpMailer
.
Query Class
The Query
Class is in the Etier::Dns
namespace, and
uses the Win32 API to obtain a list of MX records. It has a single method -
GetMx
- which uses the DnsQuery
API function. Note that this function is only included in Windows 2000 and
later. Like many DNS functions, the DnsQuery function type is implemented
in multiple forms to facilitate different encoding methods (taken from
MSDN). To convert between the LPCTSTR
and System::String
types a Util
class is included that implements ConvertStringToLPCTSTR
.
The implementation of this is not essential to the solution so I will not cover
it in this article, but was found through a search on Microsoft's public newsgroups.
The DnsQuery
function is called, passing (amongst other things)
the domain name, query type (DNS_TYPE_MX
), and a pointer to a DNS_RECORD
pointer - this is used to hold the results of the query.
GetMx Method Implementation
// 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
Provided the DnsQuery
function succeeds an ArrayList
is created that is used to contain MxRecord
types. This list is
populated whilst looping through the linked list of records contained in the
DNS_RECORD
structure. The DNS_RECORD
structure includes
struct _DnsRecord * pNext
- a pointer to the next record. By setting
the current DNS_RECORD
to pNext
its possible to go
through every record returned.
I originally believed that by issuing a DNS_TYPE_MX
query the
result would only contain MX records. However, this gave me exceptions and I
had to include a conditional to check that the current result's type (contained
in the wType
field) was DNS_TYPE_MX
. Provided it is,
then an MxRecord
structure is initialised containing the SMTP server's
address, and priority.
It's necessary to use String::Copy
to copy the string obtained
through the pNameExchange
pointer since when GetMx finishes result
will be deleted and thus the existing reference would also become invalid and
an exception will be thrown.
// 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;
It's possible that readers of this article will be from an ASP background,
primarily VB or C# and may not have an understanding of boxing and unboxing,
for their benefit I'll include a quick overview. Boxing is the process of converting
a value type to a reference type. ArrayList
's Add
method holds a pointer to any instance of Object
(or Object
derived type). Since Object
is a reference type its necessary to
box our value type (or wrap it) with a reference type. Since it involves creating
a new object on the managed heap, C++ requires the explicit use of the __box
keyword. This prevents any ambiguity as to whether it still points to the old
instance.
Unboxing can then be used to create a value type from a reference type. In
the EmailValidator
class this is done through the following code:
MxRecord mx = *dynamic_cast<__box MxRecord*>(serverList->get_Item(i));
Its necessary to tell the runtime how the reference class should be interpreted,
and this is achieved through de-referencing the pointer to the boxed MxRecord
.
Its also necessary to tell the runtime how to interpret the reference type (hence
the dynamic_cast
). Boxing and unboxing is done automatically in
C# but C++ makes it absolutely clear another object is created and thus by changing
the boxed type its not changing the original value type. Boxing and unboxing
is also a fairly intensive operation, so performing it as few times as possible
is desireable.
I decided to create my own structure as opposed to using the DNS_RECORD
to allow interoperability with other .NET applications to be as painless as
possible. For example, its possible to use the same DNS querying code from C#
without touching the Win32 API (one of the great things about .NET, and C++
.NET in particular). In fact, I produced a quick C# console application to test
the code whilst I was developing it. It also demonstrates one of the greatest
things about Visual Studio .NET - the ability to debug across languages!
Time for a quick look at the MxRecord
structure.
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 };
The structure implements the CompareTo
method of the IComparable
interface. Since the records will be put in an ArrayList
it would
be nice to be able to sort them based on their priority. To do this its necessary
to include support for the IComparable
interface. The CompareTo
method compares the current instance with another object of the same type, it
should return < 0 if the instance is less than *obj, 0 if it is equal, and
> 0 if it is greater than.
Going back to the overall solution, we now have an ArrayList
which
is populated with MxRecord
structures and its time to start connecting
to them to see whether they'll accept emails for the user. All SMTP sessions
are handled by the SmtpMailer
class which is what we will look
at next.
SmtpMailer Class
The SmtpMailer
class is based on code by Albert Pascual in his
CodeProject article "Sending
mail in Managed C++ using SMTP".
Firstly, the constructor
SmtpMailer::SmtpMailer( String *localServer, String *fromEmail ) { m_sLocalServer = localServer; m_sFromEmail = fromEmail; }
The SmtpMailer
instance stores details of the Local Server and From
Email addresses to be used throughout the connections, and these are set through
the constructor.
WillAcceptAddress
This method actually connects to the specified server, and issues commands as per the SMTP protocol. The code is largely self explanatory and should be easy to follow for anybody without C++ experience.
It uses the .NET Framework's TcpClient
to connect to a server,
and issues commands through the Server's NetworkStream
. Again,
to me streams are something I've never used before .NET and they are extremely
versatile.
After each SMTP command is issued its result (obtained through the RdStrm->ReadLine
call) is checked to determine whether the server effectively returned ok
or error. Ok values are determined by checking the first digit
of the string returned, if its between 1 and 3 then the server accepted the
command and is ready to accept the next one. If an error value is returned then
WillAcceptAddress
will return false and any connections will be
closed. This checking is performed through the IsOk
method (which
in turn uses Regular Expressions).
The important SMTP command is RCPT TO. This is used to inform the MTA of any recipients, the result of this command is used to determine whether validation should succeed.
// 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
is called for each MX record found. For example,
performing a DNS query for MX records on codeproject.com yields a single
result - mail.codeproject.com, so only a single connection is made. If that server
returns an error code during the session WillAcceptAddress
will fail
and since there's only one server validation as a whole will fail. If more servers
existed then they would also be checked.
Example Usage
The downloads includes an ASP.NET Web Application that uses the assembly. However, here is the code you would use to put the tag on a page.
Firstly its necessary to map a namespace in the assembly into the page, such that any controls can be referenced. This is done as follows:
<%@ Register TagPrefix="etier" Namespace="Etier" Assembly="SmtpSend" %>
The validator can then be included as follows
<etier:EmailValidator Id="MyValidator" Display="none" ControlToValidate="Address" ErrorMessage="* Invalid" RunAt="server" EnableClientScript="False" LocalServer="oobaloo.co.uk" FromEmail="webmaster@oobaloo.co.uk" />
The code is essentially the same as that for any validator with the exception
of the FromEmail
and LocalServer
properties which
were added.
Conclusion
Once again I am left to admire ASP.NET :) Page Validation is just one of it's features that makes web application development a real joy. I've done a fair bit of MFC development before and its great to be able to use C++ to produce ASP.NET controls (even if the managed extension syntax does make the code look a little kludgy).
Hopefully this has been useful for those in the C++ world to see that you can still use C++ with ASP.NET. It's true that ASP.NET does not include a C++ compiler, and so writing C++ code directly in ASPX files or as part of code-behind won't work. However, it is possible to produce code in assemblies and then pre-compile.
The other aim was to show those with little or no C++ experience (since its
assumed most ASP developers have a strong VB bias) why its so great. C++ .NET
is unique amongst other .NET languages in its ability to produce unmanaged or
managed code (how storage for instances is maintained) but also its strong Interoperability
support - one of the main reasons why this solution was implemented in Managed
C++ rather than C#. By using MC++ its possible to call the functions directly
by including any necessary headers (DnsQuery
is from windns.h
).
If this were to be accomplised in C# using PInvoke I would have had to included
DllImport
statements galore, as well as including definitions for
any structures and methods which can get pretty dull pretty quick.
The code is free for anybody to use and improve, I'm by no means a C++ guru so I'm sure there are bits which could be implemented more efficiently, or designed better. If you do make improvements to the code (or spot glaring errors) then it'd be great to hear from you.
References
Regular Expressions:
.NET
Framework Regular Expressions on MSDN
History
- 15/12/2002 - Thanks to a post by Timothy Glenn Stockstill I've
updated the
code to include a call to
DnsRecordListFree
. Sorry that its taken me this long to get the code and archives updated.