Parsing email files in ColdFusion / Railo / Lucee
I, like most developers, have always left the mail settings in the ColdFusion administrator set to localhost to keep email from actually getting out in the wild and, potentially, sent to real people (not that anyone would actually have live data in their Dev/QA environments, right?). Unless you have an SMTP server running on the server, that, essentially, disables the sending of email. When you send an email via CFMAIL, you end up with CFMAIL files hitting the spool directory. Once the server fails to connect to an SMTP server at localhost, those CFMAIL files are moved to the infamous “undelivr” directory. Then, when you actually need to see those emails, you can just open up the plain text CFMAIL file to see how things look.
I’d imagine that is a very common setup for developers. And, once you get used to it, it’s not really that big of a deal. One of the guys at work even wrote a quick CF wrapper for the undelivr directory that parses the CFMAIL files into a nice UI. Once that UI existed, our QA department became completely dependent on it (especially our automated testing).
Now, we are looking at upgrading to Railo or Lucee and they do not handle email the same way. Since there is a transition period where we will be using CF and Railo/Lucee, the UI needs to handle both. Ideally, we would have either server creating email files in the same way and then UI would only have to parse that new format instead of conditional logic to do things this way for one platform, that way for anther… and so on.
Enter SMTP…
If we could get an SMTP server set up that would receive email from our servers but, instead of sending it on, would write them to disk, we would have exactly what I was looking for… consistent mail files regardless of platform source. Not only consistent email files, but VALID email files that could be parsed with something like javax.mail.
I had never looked into development SMTP servers before and was sure I was going to end up writing one. I was surprised when I found as many as I did floating around out there. In the end, I found one that worked enough for us that there was no need to create one. It is also open source and gives us the opportunity to make it even better (for us).
Papercut – The Simple Desktop Email Receiver
https://papercut.codeplex.com/
Alone, Papercut is probably all most people would need. You can run it on your server, point your mail server settings to it and it will start receiving emails and writing them to disk. While it can run as a service, it also has a nice UI for viewing emails (minus attachments). It is also capable of forwarding the emails TO and FROM any address you choose (WITH attachments).
The downside to forwarding the emails through Papercut is that it really isn’t a forward; it overwrites the original TO and FROM and sends the email as new. You lose the original recipient(s) and sender from the headers completely. Unfortunately, that kills it for our automated testing. However, we can still use it for all of our manual QA and unit testing.
So, for our needs, I WILL be forwarding the emails to specific addresses so we can see the emails in our inboxes (with attachments) when performing manual unit and QA testing. But, we will also be parsing the .eml files so they can be viewed (with all original headers) in our mail UI.
Enough jibber jabber… here is the CFC to handle the parsing of the email files.
component output="true" {
public struct function parseEmail(required string emlFile, boolean saveAttachments=false, string attachmentPath){
if(!structKeyExists(arguments, 'attachmentPath') || !len(trim(arguments.attachmentpath))){
arguments.attachmentPath = "";
}
local.emlStruct = {};
if(fileExists(arguments.emlFile)){
local.props = createObject("java", "java.lang.System").getProperties();
local.props.put( javacast("string", "mail.host"), javacast("string", "fakesmtp.localhost"));
local.props.put( javacast("string", "mail.transport.protocol"), javacast("string", "smtp"));
local.mailSession = createObject("java", "javax.mail.Session").getDefaultInstance(local.props, javacast("null", ""));
local.sourceStream = createObject("java", "java.io.FileInputStream").init(arguments.emlFile);
local.mimeMessage = createObject("java", "javax.mail.internet.MimeMessage").init(local.mailSession, local.sourceStream);
local.headers = local.mimeMessage.getAllHeaderLines();
while(local.headers.hasMoreElements()){
local.thisHeader = local.headers.nextElement();
local.emlStruct.headers[listFirst(local.thisHeader, ':')] = listDeleteAt(local.thisHeader, 1, ':');
}
local.emlStruct.body = local.mimeMessage.getContent();
local.emlStruct.attachments = [];
if(isObject(local.emlStruct.body)){
if(local.emlStruct.body.getCount()-1){
if(arguments.saveAttachments){
if(!structKeyExists(arguments, 'attachmentPath') || !len(trim(arguments.attachmentPath))){
arguments.attachmentPath = listDeleteAt(arguments.emlFile, listlen(arguments.emlFile, "\/"), "\/") & "\attachments\";
}
arguments.attachmentPath = arguments.attachmentPath & rereplace(local.mimeMessage.getMessageId(), "(<|>)", "", "all") & "\";
if(!directoryExists(arguments.attachmentPath)){
directoryCreate(arguments.attachmentPath);
}
}
for(local.i=1; local.i < local.emlStruct.body.getCount(); local.i++) {
local.bodyPart = local.emlStruct.body.getBodyPart(javacast("int", local.i));
local.disposition = local.bodyPart.getDisposition();
if(structKeyExists(local, "disposition") && (ucase(local.disposition)=="ATTACHMENT" || ucase(local.disposition)=="INLINE") ){
if(arguments.saveAttachments && !fileExists(arguments.attachmentPath & local.bodyPart.getFileName())){
local.inputStream = local.bodyPart.getInputStream();
local.outputStream = createObject("java","java.io.ByteArrayOutputStream").init();
local.langByte = createObject("java", "java.lang.Byte").TYPE;
local.byteArray = createObject("java","java.lang.reflect.Array").newInstance(local.langByte, javacast("int", 1024));
local.length = local.inputStream.read(local.byteArray);
local.offset = 0;
while(local.length > 0){
local.outputStream.write( local.byteArray, local.offset, local.length);
local.length = local.inputStream.read(local.byteArray);
}
local.outputStream.close();
local.inputStream.close();
fileWrite(arguments.attachmentPath & local.bodypart.getFileName(),local.outputStream.toByteArray() );
}
local.bodyPart.addHeader('Filename',local.bodyPart.getFileName());
if(arguments.saveAttachments){
local.bodyPart.addheader("Filepath", arguments.attachmentPath & local.bodyPart.getFileName());
} else {
local.bodyPart.addheader("Filepath", "Not Saved");
}
local.fileHeaders = local.bodyPart.getAllHeaderLines();
while(local.fileHeaders.hasMoreElements()){
local.thisHeader = local.fileHeaders.nextElement();
local.fileStruct[listFirst(local.thisHeader, ':')] = listDeleteAt(local.thisHeader, 1, ':');
}
arrayAppend(local.emlStruct.Attachments, local.fileStruct);
}
}
}
local.emlStruct.body = local.emlStruct.body.getBodyPart(javacast("int",0)).getContent();
}
}
return local.emlStruct;
}
}
You will notice 3 arguments in the parseEmail() function...
emlFile - This is the absolute path to the email file that you want to parse. It is obviously required.
saveAttachments - This is a bit flag to tell the function whether or not it should save email attachments. By default, it does not write attachment files to disk
attachmentPath - if saveAttachments is true, then you can provide a path in which to save attachments. If you do not provide a path, it will be derived from the emlFile path.
The function returns a structure containing all headers, the body and an array of attachments (if saved to disk).
All attachments will be saved in [provided or derived path]/attachments/
It would be pretty easy to create a new function for serving up attachments through cfheader/cfcontent if you preferred not writing files to disk.