Sending e-mail attachments in CFMX without writing data to disk
One of the many projects I'm currently working on is some code to delay sending of e-mails until a specified window of time. We're generating some report data for clients during the offhours, but the clients want the results e-mailed no earlier than 8am.
Our reports often contain images or other attachments that need to be included e-mails. One issue I really don't like about the implementation of CFMAIL in CFMX is that it requires attachments to be written to disk before you can send the mail. This means if I want to use the CFMAIL tag to deliver delayed e-mails, I would to manage attachments until I'm sure the message is delivered. I don't like that solution, so I set out to see if there might be other ways of generating attachments from binary data in memory. This lead me to researching the JavaMail API—which is the API that CFMAIL uses behind the scenes.
I quickly learned that even CFMX v7.02 still uses JavaMail v1.3.1—which is an older version of the API. One of the issues in v1.3.1 is that it does not include any classes for taking a binary stream from memory and converting to an attachment. It comes with a FileDataSource class—which will read in a file and convert it to the correct data source. This might be the reason that Macromedia/Adobe requires the file to be written to disk.
In order for you to place an attachment in the e-mail, you must get the binary data into a DataSource. All of the solutions I found on the web, suggested writing a ByteArrayDataSource class to handle converting an in-memory binary object so that you could place it in the attachment, but I really didn't want to introduce a 3rd party dependency if I didn't have to.
After doing some digging of all the available classes included with CFMX 7, I found the org.apache.axis.attachments.OctetStreamDataSource class which I thought might get me what I needed—and it turns out I was write. Utilizing the OctetStreamDataSource I was able to convert any binary data stored in a CF variable into the correct DataSource that the JavaMail API requires. This means I can now use tags like CFDOCUMENT or CFCHART to generate data on the fly and send it in an e-mail without ever writing it to disk.
Below is some source code that will show you how to use the JavaMail API to send a multipart message that contains:
- Plain Text
- HTML (with embedded images)
- A file attachment from a file on disk
- A file attachment from binary data stored in memory
I've tried to comment each line to fully explain whats going on. This is the exact code I was using from my test script. Just remember to update the variables in the "config" section to use your mail server.
If you run the code below, it will actually send the e-mail and output the SMTP message envelope. The SMTP message envelope could be written to most SMTP server's queue or pickup folder to be delivered. For example, you could write this file to IIS's SMTP service's "pickup" folder and the SMTP service would automatically queue the message for delivery. You wouldn't even have to connect to the SMTP server.
Hopefully somebody else finds this code useful!
<cfdocument format="pdf" name="binPdf">
<h1>
A PDF Document
</h1>
<p>
This is a document rendered by the cfdocument tag
at <cfoutput>#dateFormat(now(), "mmmm dd, yyyy")#
#lCase(timeFormat(now(), "h:mmtt"))#</cfoutput>.
</p>
<table width="50%" border="2" cellspacing="2" cellpadding="2">
<tr>
<td><strong>Name</strong></td>
<td><strong>Role</strong></td>
</tr>
<tr>
<td>Bill</td>
<td>Lead</td>
</tr>
<tr>
<td>Susan</td>
<td>Principal Writer</td>
</tr>
<tr>
<td>Adelaide</td>
<td>Part Time Senior Writer</td>
</tr>
<tr>
<td>Thomas</td>
<td>Full Time for 6 months</td>
</tr>
<tr>
<td>Michael</td>
<td>Full Time for 4 months</td>
</tr>
</table>
</cfdocument>
<!---// save the file to the following path and file //--->
<cfset sFilename = expandPath('./test-file.pdf') />
<!---// write the file to disk //--->
<cffile action="write" file="#sFilename#" output="#binPdf#">
<!---// create a png chart and save it to a variable //--->
<cfchart name="binChart" format="png" font="arialunicodeMS" xaxistitle="Month" yaxistitle="Degrees Celsius" showlegend="yes">
<cfchartseries type="line" serieslabel="Europe">
<cfloop index="i" list="Apr,May,Jun,Jul,Aug,Sep">
<cfchartdata item="#i#" value="#RandRange(12,42)#">
</cfloop>
</cfchartseries>
<cfchartseries type="line" serieslabel="USA">
<cfloop index="j" list="Apr,May,Jun,Jul,Aug,Sep">
<cfchartdata item="#j#" value="#RandRange(12,42)#">
</cfloop>
</cfchartseries>
</cfchart>
<cfscript>
// config
sMailServer = "mail.yourserver.com";
sUsername = "";
sPassword = "";
sSubject = "Using the JavaMail API!";
sAddyTo = "to@yourcompany.com";
sAddyFrom = "from@yourcompany.com";
// set javamail properties
oProps = createObject("java", "java.util.Properties").init();
oProps.put("javax.mail.smtp.host", sMailServer);
// get static recipient types
oRecipientType = createObject("java", "javax.mail.Message$RecipientType");
// create the session for the smtp server
oMailSession = createObject("java", "javax.mail.Session").getInstance(oProps);
// create a new MIME message
oMimeMessage = createObject("java", "javax.mail.internet.MimeMessage").init(oMailSession);
// create the to and from e-mail addresses
oAddressFrom = createObject("java", "javax.mail.internet.InternetAddress").init(sAddyFrom);
oAddressTo = createObject("Java", "javax.mail.internet.InternetAddress").init(sAddyTo);
// build message
// set who the message is from
oMimeMessage.setFrom(oAddressFrom);
// add a recipient
oMimeMessage.addRecipient(oRecipientType.TO, oAddressTo);
// set the subject of the message
oMimeMessage.setSubject(sSubject);
// create multipart message: only needed if you're including both plain/text and html
// or using attachments
oMimeMultipart = createObject("java", "javax.mail.internet.MimeMultipart").init();
// specifies that the message contains both inline text and html, this is so that
// images given a cid will show up when rendered by the e-mail client
oMimeMultipart.setSubType("related");
// create plain text multipart
oPlainText = createObject("java", "javax.mail.internet.MimeBodyPart").init();
// create the plain/text for the message
oPlainText.setText("You like using JavaMail in CFMX.");
// add the body part to the message
oMimeMultipart.addBodyPart(oPlainText);
// create html text multipart
oHtml = createObject("java", "javax.mail.internet.MimeBodyPart").init();
// add the html content (the setText() method shortcut/only works for "plain/text")
oHtml.setContent(
"<html><head><title>HTML E-mail</title></head><body>"
& "<h1>You like using JavaMail in CFMX.</h1>"
& "<p><img src=cid:23abc@pc27 /></p>"
& "</body></html>",
"text/html"
);
// add the body part to the message
oMimeMultipart.addBodyPart(oHtml);
// attach an inline binary object
att = createObject("java", "javax.mail.internet.MimeBodyPart").init();
// create an octet stream out of the binary file
os = createObject("java", "org.apache.axis.attachments.OctetStream").init(binPdf);
// we now convert the octet stream into the required data source. using an octet stream
// allows us pass in any binary data as a file attachment
osds = createObject("java", "org.apache.axis.attachments.OctetStreamDataSource").init("", os);
// initialize the data handler using the data source
dh = createObject("java", "javax.activation.DataHandler").init(osds);
// pass in the binary object to the message--javamail will handle the encoding
// based on the headers
att.setDataHandler(dh);
// define this binary object as a PDF
att.setHeader("Content-Type", "application/pdf");
// make sure the binary data gets converted to base64 for delivery
att.setHeader("Content-Transfer-Encoding", "base64");
// specify the binary object as an attachment
att.setHeader("Content-Disposition", "attachment");
// define the name of the file--this is what the filename will be in the e-mail client
att.setFileName("test-binary_var.pdf");
// add the body part to the message
oMimeMultipart.addBodyPart(att);
// attach an inline binary object
png = createObject("java", "javax.mail.internet.MimeBodyPart").init();
// create an octet stream out of the binary file
os = createObject("java", "org.apache.axis.attachments.OctetStream").init(binChart);
// we now convert the octet stream into the required data source. using an octet stream
// allows us pass in any binary data as a file attachment
osds = createObject("java", "org.apache.axis.attachments.OctetStreamDataSource").init("", os);
// initialize the data handler using the data source
dh = createObject("java", "javax.activation.DataHandler").init(osds);
// pass in the binary object to the message--javamail will handle the encoding
// based on the headers
png.setDataHandler(dh);
// define this binary object as a PNG
png.setHeader("Content-Type", "image/png");
// make sure the binary data gets converted to base64 for delivery
png.setHeader("Content-Transfer-Encoding", "base64");
// specify the binary object as an attachment
png.setHeader("Content-Disposition", "attachment");
// this is the cid you referenced in the html body
png.setHeader("Content-ID","<23abc@pc27>");
// define the name of the file--this is what the filename will be in the e-mail client
png.setFileName("chart-binary_var.png");
// add the body part to the message
oMimeMultipart.addBodyPart(png);
// build attachment - file
fatt = createObject("java", "javax.mail.internet.MimeBodyPart").init();
// create a data source directly from a file on the OS
fds = createObject("java", "javax.activation.FileDataSource").init(sFilename);
// initialize the data handler using the data source
dh = createObject("java", "javax.activation.DataHandler").init(fds);
// pass in the file object to the message--javamail will handle the encoding
// based on the headers
fatt.setDataHandler(dh);
// define this file as a PDF
fatt.setHeader("Content-Type", "application/pdf");
// make sure the file gets converted to base64 for delivery
fatt.setHeader("Content-Transfer-Encoding", "base64");
// specify the file as an attachment
fatt.setHeader("Content-Disposition", "attachment");
// define the name of the file--get the file name from original file
fatt.setFileName(fds.getName());
// add the body part to the message
oMimeMultipart.addBodyPart(fatt);
// place all the multi-part sections into the body of the message
oMimeMessage.setContent(oMimeMultipart);
// in this section we'll build the message into a string. you could dump
// the string to a file in most SMTP server's queue file for delivery
// this is exactly what would be pass to the SMTP server
// create a bytearray for output
outStream = createObject("java", "java.io.ByteArrayOutputStream");
// create a budder for the output stream
outStream.write(repeatString(" ", 1024).getBytes());
// save the contents of the message to the output stream
oMimeMessage.writeTo(outStream);
// save the contents of the message to the sMailMsg variable
sMailMsg = outStream.toString();
// reset the output stream (for stability)
outStream.reset();
// close the output stream
outStream.close();
// create a transport to actually send the message via SMTP
oTransport = oMailSession.getTransport("smtp");
// connect to the SMTP server using the parameters supplied; use
// a blank username and password if authentication is not needed
oTransport.connect(sMailServer, sUsername, sPassword);
// send the message to all recipients
oTransport.sendMessage(oMimeMessage, oMimeMessage.getAllRecipients());
// close the transport
oTransport.close();
</cfscript>
<cfcontent reset="true" /><cfoutput>#htmlCodeFormat(trim(sMailMsg))#</cfoutput>
<cfabort />
Comments
Excellent job. This is very useful, and I know there has been some rumblings of late inside Adobe on this topic. Great to see a solution!
I will say this, one of the benefits of requiring a file on a physical disk, is if you're blasting out thousands of e-mails, it helps keeps the file size down of each message in the spool folder.
I'd just like to see the option to attach in memory binary objects using cfmailpart. It definitely would be helpful at times.
We've previous run into problems w/e-mail messages getting stuck in the queue and then having their attachment files being deleted, which means we can no longer send the e-mail. Very frustrating.
I'll add that for readers who are interested in learning more about the ability of CFMX to permit you to write the results of CFDOCUMENT and CFREPORT to a variable, thus to be able to email this way, I did a blog entry on it recently:
http://carehart.org/blog/client/index.cfm/2007/1/1...
Thanks,
CFFILE shouldn't have a problem accessing files on the network (well at least I've never seen a problem using Windows Servers.)
If you're having issues, just do a Google search for something like:
"cfmx accessing files on a mapped drive"
Check it out at:
http://timarcher.com/?q=node/53">http://timarcher.com/?q=node/53
Very cool. I am trying to solve this same problem, but using the soloution within Actionscript for Flash development. You can write image files to byte Arrays in Flash and I am trying to find a way to email that data directly from memory. I don't know if I can call up a Java API in Actionscript, but this was very helpfule in terms of my understandig the basic process. Thanks.
What I'd recommend is using Flash Remoting (AMF) if your server provides support for it. Otherwise, use one of the other Remoting Services (i.e. Web Services) to talk with your server.
http://blog.pengoworks.com/blogger/index.cfm?actio...
This is great stuff! I have a requirement to send a list of reports via email to a client. I had tried various approaches until I came across your blog. It's a real time saver.
Thanks so much.
My form is at www.rochesterpubliclibrary.org/apps/forms/AudSetup...
I fill in the data on the form, but the email/attachment just gives me Contact: #form.groupcontact# without the form data being filled it. I am a librarian, not a programmer so I'm sure i'm missing something..
You need to wrap your output in <cfoutput> tags to have CF process those variables, thus:
Contact: #form.groupcontact#
would be:
<cfoutput>
Contact: #form.groupcontact#
</cfoutput>
Thanks

Thanks,!