|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis 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 OverviewIt 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> 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 It's worth noting that the SMTP protocl does include support for a Implementation OverviewThe 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 ClassThe Methods
Properties
Query ClassThe Query class implements a single method - SmtpMailer ClassThe The main method is MxRecord StructureWhilst 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 The structure also includes support for the Solution ImplementationEmailValidator ClassControlPropertiesValidThe code is fairly self explanatory, it obtains a reference to the Validator's
associated control (which ought to be a // 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; } EvaluateIsValidThe 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 SMTP Connections are handled by the 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 ClassThe The 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 I originally believed that by issuing a It's necessary to use // 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. Unboxing can then be used to create a value type from a reference type. In
the 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 I decided to create my own structure as opposed to using the Time for a quick look at the 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 Going back to the overall solution, we now have an SmtpMailer ClassThe Firstly, the constructor SmtpMailer::SmtpMailer( String *localServer, String *fromEmail ) { m_sLocalServer = localServer; m_sFromEmail = fromEmail; }
The WillAcceptAddressThis 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 After each SMTP command is issued its result (obtained through the 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; }
Example UsageThe 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 ConclusionOnce 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 ( 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. ReferencesRegular Expressions: History
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||