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.
<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>...Read More