Sending e-mail attachments in CFMX without writing data to disk

Posted by Dan on Feb 8, 2007 @ 11:14 AM

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!

<!---// create a PDF document and save it to a variable //--->
<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 />
Categories: HTML/ColdFusion, Java, Source Code

19 Comments

  • Dan, very cool! I have never seen most of things you have written about. It's gonna take me some time to fully get through it, but this is waaaay cool.

    Thanks,!
  • Dan,

    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!
  • @Sami:

    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.
  • This is so great. Thanks, Dan.

    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...
  • Dan i have a question what about the file to be attach is on the other pc? how to handle this situation?

    Thanks,
  • @John:

    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"
  • I also have an example showing how to use JavaMail to send emails with attachments. It also supports relaying through an SMTP server that requires authentication.
    Check it out at:
    http://timarcher.com/?q=node/53">http://timarcher.com/?q=node/53
  • Hey Dan,

    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.
  • @Garth:

    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.
  • I've added a new blog entry that shows a technique pointed out by Jon Wolski that uses just the CFMAIL and CFMAILPART tags:

    http://blog.pengoworks.com/blogger/index.cfm?actio...
  • Dan,

    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.
  • This is GREAT! Thanks so much for sharing. I finally got it to work but now have a stupid question. Am I able to pull in data from a form and get this to email the form data? Example: Contact: #form.groupcontact#
    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..
  • @Susan:

    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>
  • DUH.... THANK YOU! THANK YOU! THANK YOU!. I've been trying to get this to work FOREVER. You saved me countless hours of frustration. Now I can cross that one off the list...
  • Very cool. Is it possible to specify a dataset (ie: query attribute in cfmail) using JavaMail? I'm looking for a faster alternative than CFMail.

    Thanks
  • Nice! Thanks.
  • Thanks for posting this Dan.
    I'm having a problem with send mixed content in my emails.
    I have a combination of HTML with embedded images, as well as mutliple PDF attachments.
    When I send it through CFMail, outlook does not display the paperclip showing that there are attachments with the email.

    The only way I got the paperclip to show up was to use your JavaMail example, and then modify it.
    I had to set the subType to mixed, and then I googled the JavaMail API to find out how to include an image attachment, in inline html, since your example did not explain that.

    The problem with the JavaMail API version is that since the subtype is NOT "related", the image (which is the logo header of the html email) comes in as an attachment also.

    I have found no solution for this on the internet. I'm surprised no one else has had an issue with this in CF.
    It seems pretty common to me that people would be sending HTML emails that have images that have to be displayed inline, as well as multiple file attachments that need to be sent. However, for some reason, the out of the box solution with CFMail, causes outlook to not show the paperclip icon.
  • @Ali:

    What version of CF are you using?

    CF8 and above should support attachments from memory via the <cfmailparam /> and that tag has a "disposition" attribute, which you can set to either "attachment" or "inline". This should allow you to do what you want.

    So you should be able to do what you want w/CF8+.
  • I've got the same issue as Ali. Running with CF8, HTML emails with inline logo and a separate attachment. I tried the disposition for the attachment as both attachment and inline, and still the paperclip icon does not appear (in MS Outlook express).

Comments for this entry have been disabled.