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 uses Control::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.
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;
Regex* r = new Regex(S"^*@(?<domain>\\S+)");
if (r->IsMatch( _emailAddressBox->Text ))
domainName = r->Match( _emailAddressBox->Text )->
Result("${domain}")->ToString();
else
return false;
ArrayList* serverList = Etier::Dns::Query::GetMx( domainName );
serverList->Sort();
SmtpMailer *mail = new SmtpMailer( m_sLocalServer, m_sFromEmail );
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
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.
ArrayList* aHostList = new ArrayList();
if (SUCCEEDED(status))
{
if (result!=0)
{
while ( (result->pNext!=NULL) && (result->pNext != result))
{
if (DNS_TYPE_MX == result->wType)
{
MxRecord mx;
mx.NameExchange = String::Copy(((String*)(LPSTR)result->
Data.MX.pNameExchange) );
mx.nPriority = result->Data.MX.wPreference;
aHostList->Add( __box(mx) );
}
result = result->pNext;
}
}
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
{
int CompareTo( System::Object *obj )
{
MxRecord mx = *dynamic_cast<__box MxRecord*>(obj);
return this->nPriority - mx.nPriority;
}
String *NameExchange;
int nPriority;
};
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.
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() ))
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;
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.