Tuesday, April 27, 2010

ColdFusion: Verify SMTP, POP3 and IMAP Mail Server Connection

I saw an intriguing question about mail servers this week, on stackoverflow.com. The question was is it possible to verify an SMTP mail server connection programatically? Essentially duplicate the "Verify mail server connection" functionality that exists in the CF Administrator. Though I am certain this is old news to some of you, the answer is yes.

Interestingly, both suggestions on stackoverflow.com utilized java libraries already built into all three engines: Adobe ColdFusion, Railo and OpenBlueDragon. Member folone suggested using the SMTPClient class in Apache Commons Net , while member sfussenegger suggested using the core JavaMail library with an Authenticator. (Unfortunately, I do not know their full names).

Having used JavaMail and Authenticators before, I was somewhat familiar with that option. Though a bit rusty. Being less familiar with Commons Net, I had to do some research. Apparently, Commons Net is the predecessor of JavaMail. So the two libraries serve similar purposes, but Commons Net is a lower level API. However, both can be used to communicate with SMTP and POP3 servers. Though I do not think the Commons library supports IMAP.

Anyway, I was curious about the Commons classes, so I played around with them a bit. Eventually I managed to force an SSL connection with POP3, courtesy of this tip and the convenience MailSSLSocketFactory class. However, the code only works in Adobe CF. Unfortunately, Railo and OpenBlueDragon do not seem to include the MailSSLSocketFactory class, and I did not want to create my own.

<cfscript> 
   isVerified = false;
   try {
      SSLFactory = createObject("java", "com.sun.mail.util.MailSSLSocketFactory").init("SSL");
      POP3Client = createObject("java", "org.apache.commons.net.pop3.POP3Client").init();
      POP3Client.setSocketFactory( SSLFactory );
      POP3Client.connect("pop.gmail.com", 995);
      isVerified = POP3Client.login("user@gmail.com", "user password");
      WriteOutput("isVerified="& isVerified &"<br />");
      POP3Client.logout();
   }
   // error occurred during login or logout
   catch(java.io.IOException e) {
      WriteDump(e);
   }
</cfscript> 

Since JavaMail works on all three engines, with no extra classes, it seemed the best choice. Plus I really liked the idea of an all-in-one validator, not just smtp. So I put together a rough function that validates SMTP, POP3 and IMAP connections. It should be compatible with CF8, CF9, Railo and OpenBlueDragon. Tested against gmail and hMailServer.

For those not familiar with JavaMail, the function is very simple. It first stores the properties you want to use for the connection (like TLS). Then creates a new mail Session using those properties. Next it attempts to open a connection to the given mail server and requests authentication. Finally the connection is closed. If an error occurs, the function checks the exception type to determine if the credentials were invalid or it was some other type failure. The function returns a structure with three keys: WasValidated, ErrorType and ErrorDetail.

One last note about the function. When TLS is selected, it is made mandatory. So if the mail server on the other end does not support TLS, the verification will fail, for security reasons. But you can change that behavior if you wish.

If you are interested in reading more about JavaMail, these two articles are very comprehensive: jGuru: Fundamentals of the JavaMail API and JavaMail FAQ's.

Example
<cfset response = verifyMailServer(    host     = "smtp.gmail.com",
                                          protocol = "smtp",
                                          port     = 587,    
                                          user     = "username@gmail.com",
                                          password = "user password",
                                          useTLS     = true,
                                          debug    = true,
                                          overwrite = false,
                                          logPath  = "c:\verifyMailServer_Test.log"
                                   ) />
<cfdump var="#response#" label="Verfication Results">

Function
<cffunction name="verifyMailServer" returntype="struct" access="public" output="true">
    <cfargument name="protocol" type="string" required="true" hint="Mail protocol: SMTP, POP3 or IMAP" />
    <cfargument name="host" type="string" required="true" hint="Mail server name (Example: pop.gmail.com)"/>
    <cfargument name="port" type="numeric" default="-1" hint="Mail server port number. Default is -1, meaning use the default port for this protocol)" />
    <cfargument name="user" type="string" required="true" hint="Mail account username" />
    <cfargument name="password" type="string" required="true" hint="Mail account password" />
    <cfargument name="useSSL" type="boolean" default="false" hint="If true, use SSL (Secure Sockets Layer)" >
    <cfargument name="useTLS" type="boolean" default="false" hint="If true, use TLS (Transport Level Security)" >
    <cfargument name="enforceTLS" type="boolean" default="false" hint="If true, require TLS support" >
    <cfargument name="timeout" type="numeric" default="0" hint="Maximum milliseconds to wait for connection. Default is 0 (wait forever)" />
    <cfargument name="debug" type="boolean" default="false" hint="If true, enable debugging. By default information is sent to is sent to System.out." >
    <cfargument name="logPath" type="string" default="" hint="Send debugging output to this file. Absolute file path. Has no effect if debugging is disabled." >
    <cfargument name="append" type="boolean" default="true" hint="If false, the existing log file will be overwritten" >

    <cfset var status         = structNew() />
    <cfset var props         = "" />
    <cfset var mailSession     = "" />
    <cfset var store         = "" />
    <cfset var transport    = "" />
    <cfset var logFile        = "" />
    <cfset var fos             = "" />
    <cfset var ps             = "" />
    
    <!--- validate protocol --->
    <cfset arguments.protocol = lcase( trim(arguments.protocol) ) />
    <cfif not listFindNocase("pop3,smtp,imap", arguments.protocol)>
        <cfthrow type="IllegalArgument" message="Invalid protocol. Allowed values: POP3, IMAP and SMTP" />
    </cfif>
    
    <cfscript>
        // initialize status messages
        status.wasVerified     = false;
        status.errorType      = "";
        status.errorDetail  = "";

        try {
               props = createObject("java", "java.util.Properties").init();

               // enable securty settings
               if (arguments.useSSL or arguments.useTLS) {

                    // use the secure protocol
                    // this will set the property mail.{protocol}.ssl.enable = true
                    if (arguments.useSSL) {
                         arguments.protocol = arguments.protocol &"s";            
                    }
                
                    // enable identity check
                    props.put("mail."& protocol &".ssl.checkserveridentity", "true");

                    // enable transport level security and make it mandatory
                    // so the connection fails if TLS is not supported
                    if (arguments.useTLS) {
                         props.put("mail."& protocol &".starttls.required", "true");
                         props.put("mail."& protocol &".starttls.enable", "true");
                    }
               }

               // force authentication command
               props.put("mail."& protocol &".auth", "true");
            
               // for simple verifications, apply timeout to both socket connection and I/O 
               if (structKeyExists(arguments, "timeout")) {
                    props.put("mail."& protocol &".connectiontimeout", arguments.timeout);
                    props.put("mail."& protocol &".timeout", arguments.timeout);
               }

               // create a new mail session 
               mailSession = createObject("java", "javax.mail.Session").getInstance( props );

               // enable debugging
               if (arguments.debug) {
                   mailSession.setDebug( true );
                   
                   // redirect the output to the given log file
                   if ( len(trim(arguments.logPath)) ) {
                        logFile = createObject("java", "java.io.File").init( arguments.logPath );
                        fos      = createObject("java", "java.io.FileOutputStream").init( logFile, arguments.overwrite );
                        ps       = createObject("java", "java.io.PrintStream").init( fos ); 
                        mailSession.setDebugOut( ps );
                   }
               }
            
               // Connect to an SMTP server ... 
               if ( left(arguments.protocol, 4) eq "smtp") {

                    transport = mailSession.getTransport( protocol );
                    transport.connect(arguments.host, arguments.port, arguments.user, arguments.password);
                    transport.close();
                    // if we reached here, the credentials should be verified
                    status.wasVerified     = true;

               }
               // Otherwise, it is a POP3 or IMAP server
               else {

                    store = mailSession.getStore( protocol );
                    store.connect(arguments.host, arguments.port, arguments.user, arguments.password);
                    store.close();
                    // if we reached here, the credentials should be verified
                    status.wasVerified     = true;

               }         

         }
         //for authentication failures
         catch(javax.mail.AuthenticationFailedException e) {
                   status.errorType     = "Authentication";
                 status.errorDetail     = e;
            }
         // some other failure occurred like a javax.mail.MessagingException
         catch(Any e) {
                 status.errorType     = "Other";
                 status.errorDetail     = e;
         }


         // always close the stream ( messy work-around for lack of finally clause prior to CF9...)
         if ( not IsSimpleValue(ps) ) {
               ps.close();
         }

         return status;
    </cfscript>
</cffunction>

0 comments:

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep