Ask Ben: Upload And Email File Using ColdFusion

Posted July 17, 2007 at 9:28 AM

Tags: ColdFusion, Ask Ben

I have been asked several times for a good file upload and email tutorial in ColdFusion and I have usually pointed people towards partial solutions cause I didn't have anything great to show them. Then, when I saw Will Peavy ask about this topic over on the CF-Talk list, I figured, I should just sit down and write an example that I can send people to view.

For those of you have been doing ColdFusion for a while, this will be review, but for the beginners out there, this is a fairly comprehensive example on how to use ColdFusion to upload a file, validate data, keep the file system clean, and then send that file to a given email address as a mail attachment. This ColdFusion example uses a lot of error handling. In fact, this is more error handling than I usually do (shame shame), but it is in this example to really point out the places where errors might occur and how you might want to approach handling them.

This example has a form where a web user can enter their name, email and upload a resume file. The file can be either a PDF, DOC, or RTF file type. Once uploaded, an alert is sent, via email, that the resume has been uploaded.

 Launch code in new window » Download code as text file »

  • <!--- Kill extra output. --->
  • <cfsilent>
  •  
  • <!--- Param FORM variables. --->
  • <cfparam
  • name="FORM.name"
  • type="string"
  • default=""
  • />
  •  
  • <cfparam
  • name="FORM.email"
  • type="string"
  • default=""
  • />
  •  
  • <cfparam
  • name="FORM.resume"
  • type="string"
  • default=""
  • />
  •  
  • <!---
  • For the form submission flag, since we are asking
  • it to be of type numeric, we have to catch the
  • CFParam in case someone has hacked the HTML and
  • altered the value being sent (a non-numeric value
  • will throw a ColdFusion error).
  • --->
  • <cftry>
  • <cfparam
  • name="FORM.submitted"
  • type="numeric"
  • default="0"
  • />
  •  
  • <!--- Catch CFParam data type errors. --->
  • <cfcatch>
  • <cfset FORM.submitted = 0 />
  • </cfcatch>
  • </cftry>
  •  
  •  
  • <!--- Define an array to catch the form errors. --->
  • <cfset arrErrors = ArrayNew( 1 ) />
  •  
  •  
  • <!--- Check to see if the form has been submitted. --->
  • <cfif FORM.submitted>
  •  
  • <!---
  • Now that the form has been submitted, we need
  • to validate the data.
  • --->
  • <cfif NOT Len( FORM.name )>
  •  
  • <cfset ArrayAppend(
  • arrErrors,
  • "Please enter your name"
  • ) />
  •  
  • </cfif>
  •  
  • <!--- Validate email. --->
  • <cfif NOT IsValid( "email", FORM.email )>
  •  
  • <cfset ArrayAppend(
  • arrErrors,
  • "Please enter a valid email address"
  • ) />
  •  
  • </cfif>
  •  
  • <!---
  • When it comes to validating the resume, we want to
  • check to see if they selected one. Then, once they
  • selected one, we ONLY want to mess with it if there
  • are no errors caused by other form fields.
  • --->
  • <cfif NOT Len( FORM.resume )>
  •  
  • <cfset ArrayAppend(
  • arrErrors,
  • "Please select a resume to upload"
  • ) />
  •  
  • <cfelseif ArrayLen( arrErrors )>
  •  
  • <!---
  • The file has been selected, but there are errors
  • caused by other parts of the form validation.
  • Therefore, we now have a file that is just
  • sitting in our temp folder. Delete this file to
  • keep a clean server.
  • --->
  • <cftry>
  • <cffile
  • action="DELETE"
  • file="#FORM.resume#"
  • />
  •  
  • <cfcatch>
  • <!--- File delete error. --->
  • </cfcatch>
  • </cftry>
  •  
  • <cfelse>
  •  
  • <!---
  • The resume has been selected and there are no
  • other errors caused by Form validation.
  • Therefore, we can now deal with the file upload.
  • There is a chance that the file upload will
  • cause an error, so be sure to wrap all file
  • actions in CFTry / CFCatch blocks.
  • --->
  • <cftry>
  • <cffile
  • action="UPLOAD"
  • filefield="resume"
  • destination="#GetTempDirectory()#"
  • nameconflict="MAKEUNIQUE"
  • />
  •  
  • <!---
  • Now that we have the file uploaded, let's
  • check the file extension. I find this to be
  • better than checking the MIME type as that
  • can be inaccurate (so can this, but at least
  • it doesn't throw a ColdFusion error).
  • --->
  • <cfif NOT ListFindNoCase(
  • "pdf,doc,rtf",
  • CFFILE.ServerFileExt
  • )>
  •  
  • <cfset ArrayAppend(
  • arrErrors,
  • "Only PDF, DOC, and RTF file formats are accepted"
  • ) />
  •  
  • <!---
  • Since this was not an acceptable file,
  • let's delete the one that was uploaded.
  • --->
  • <cftry>
  • <cffile
  • action="DELETE"
  • file="#CFFILE.ServerDirectory#\#CFFILE.ServerFile#"
  • />
  •  
  • <cfcatch>
  • <!--- File Delete Error. --->
  • </cfcatch>
  • </cftry>
  •  
  • </cfif>
  •  
  • <!--- Catch any file errors. --->
  • <cfcatch>
  •  
  • <!---
  • There was some sort of error with the
  • file upload. Set the error and then try
  • to delete the file.
  • --->
  • <cfset ArrayAppend(
  • arrErrors,
  • "There was a problem uploading your resume"
  • ) />
  •  
  • <!---
  • Try to delete the file. Again, we want
  • to use CFTry / CFCatch whenever dealing
  • with files, especially if we don't know
  • if the file is even at the given path.
  • --->
  • <cftry>
  • <cffile
  • action="DELETE"
  • file="#CFFILE.ServerDirectory#\#CFFILE.ServerFile#"
  • />
  •  
  • <cfcatch>
  • <!--- File delete errors. --->
  • </cfcatch>
  • </cftry>
  •  
  • </cfcatch>
  • </cftry>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now that we have validated our form data, let's
  • check to see if there are any form validation
  • errors. Only if there are no errors do w want to
  • continue processing the data - otherwise, we want
  • to skip this next part and let the form re-render.
  • --->
  • <cfif NOT ArrayLen( arrErrors )>
  •  
  • <!--- Create a short hand for the file. --->
  • <cfset strFilePath = (
  • CFFILE.ServerDirectory & "\" &
  • CFFILE.ServerFile
  • ) />
  •  
  •  
  • <cfmail
  • to="ben@xxxxxxxx.com"
  • from="#FORM.email#"
  • subject="Web Site Resume Submission"
  • type="html">
  •  
  • <p>
  • The following resum&eacute; has been
  • submitted through the web site on
  • #DateFormat( Now(), "mmm d, yyyy" )# at
  • #TimeFormat( Now(), "h:mm TT" )#.
  • </p>
  •  
  • <p>
  • Name: #FORM.name#
  • </p>
  •  
  • <p>
  • Email: #FORM.email#
  • </p>
  •  
  • <p>
  • Resum&eacute;: <em>See attached file</em>
  • </p>
  •  
  •  
  • <!--- Attach the file. --->
  • <cfmailparam
  • file="#strFilePath#"
  • />
  •  
  • </cfmail>
  •  
  •  
  • <!---
  • Delete the resume file since we no longer need
  • it on the server. HOWEVER, this will only work
  • if the emails are NOT getting spooled. If the
  • email is getting spooled, then we will end up
  • deleting the file before it had a chance to get
  • attached to the outgoing email and the mail
  • will end up failing after it leaves ColdFusion.
  •  
  • *** Put back in ONLY if SpoolEnable="no" in
  • your CFMail tag.
  • --->
  • <!---
  • <cftry>
  • <cffile
  • action="DELETE"
  • file="#strFilePath#"
  • />
  •  
  • <cfcatch></cfcatch>
  • </cftry>
  • --->
  •  
  •  
  • <!---
  • At this point, you would probably forward
  • the user to another page using something
  • like CFLocation.
  • --->
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  • </cfsilent>
  •  
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>Upload And Email A File ColdFusion Example</title>
  • </head>
  • <body>
  •  
  • <cfoutput>
  •  
  • <!--- Check to see if we have any form errors. --->
  • <cfif ArrayLen( arrErrors )>
  •  
  • <h3>
  • Please review the following:
  • </h3>
  •  
  • <ul>
  • <cfloop
  • index="intError"
  • from="1"
  • to="#ArrayLen( arrErrors )#"
  • step="1">
  •  
  • <li>
  • #arrErrors[ intError ]#
  • </li>
  •  
  • </cfloop>
  • </ul>
  •  
  • </cfif>
  •  
  •  
  • <form
  • action="#CGI.script_name#"
  • method="post"
  • enctype="multipart/form-data">
  •  
  • <!--- Our form submission flag. --->
  • <input type="hidden" name="submitted" value="1" />
  •  
  • <label for="name">
  • Name:
  •  
  • <input
  • type="text"
  • name="name"
  • id="name"
  • value="#FORM.name#"
  • />
  • </label>
  • <br />
  •  
  • <label for="email">
  • Email:
  •  
  • <input
  • type="text"
  • name="email"
  • id="email"
  • value="#FORM.email#"
  • />
  • </label>
  • <br />
  •  
  • <label for="resume">
  • Resum&eacute;:
  •  
  • <input
  • type="file"
  • name="resume"
  • id="resume"
  • value="#FORM.resume#"
  • />
  • </label>
  • <br />
  •  
  • <input type="submit" value="Upload Resum&eacute;" />
  •  
  • </form>
  •  
  • </cfoutput>
  •  
  • </body>
  • </html>

In this example, I am doing my best to try and clean up the server by deleting files when they are invalid or when ColdFusion errors get thrown. This way, I don't have random garbage files just hanging out, taking up space in my temp folders. In reality, this is probably overkill. The better solution would probably be to have a temp directory within in your site that these files are written to. This temp directory would then be cleaned out every day, or every few days to get rid of the garbage files. I say that this is the "better" solution because:

  1. It will make the code more readable (less code to handle the files means more readable / more maintainable).
  2. It will remove any issues caused by spooling emails.
  3. A temp directory (local to your site) is probably easier to get to for inspection than the GetTempDirectory() directory.

One of the benefits I point out above is that we wouldn't have to worry about email spooling. For those of you who don't know what spooling is, it's basically the way the mail server queues the outgoing mail requests. When ColdFusion executes a CFMail tag, it creates a mail object and puts it in the spool folder (or maybe the mail server does that, not sure). The mail object then sits there. The mail server, when it has time, will go through the spool directory and send out the mail objects. This means that there is a potential (and likely) time delay between when you execute the CFMail tag and when the mail server actually sends out your email.

This is important to understand when we are attaching files to emails. If we try to delete a file attachment after sending out the CFMail, chances are the mail will never get sent. The problem here is that the file doesn't actually get attached until the mail gets sent by the mail server. Therefore, if we delete a file after attaching it, we are likely deleting the file while the mail is still in the spool. Then, when the mail server goes to send out the email, it can't find the file attachment and the outgoing mail will fail.

By putting files into a local temp directory and cleaning it out periodically, we don't have to worry about efficient file deleting. Therefore, we don't have to worry about deleting files right after they were mailed out and we can let the mail sit in the spool without worrying about corrupting the data.

The other option is to turn off spooling for your outgoing email. If you add the attribute:

SpoolEnable="No"

... to your CFMail tag, it will request that the mail server not queue it in the spool. Therefore, once the CFMail tag fully executes, you are guaranteed to have already sent the file and any post-CFMail file deleting will not affect the outgoing mail. Of course, I would say that disabling the spool is probably not a great idea as you might be putting undue stress on the mail server.

Once the file is uploaded, I am validating it against the file extension. I would say that this is a better way than using the accepted mime type attribute of the CFFile tag. MIME types are not always accurate and might end up giving you false-negatives. Furthermore, MIME type checking will cause ColdFusion to throw an error if an unaccepted MIME type is uploaded. This just gives you more error handling to worry about. So, while file extensions can be altered, at least you it won't throw an error and you will have more options to work with when validating.

I like to do the file validation as the last part of the form data validation. I find that this requires less overhead since we won't do any big processing until we know the rest of the form has already been validated.

Once you have validated and processed the form data, you probably want to redirect the user to some sort of confirmation page. I have put that in my code comments, but I did not include any CFLocation example.

Download Code Snippet ZIP File

Comments (15)  |  Post Comment  |  Ask Ben  |  Permalink  |  Other Searches  |  Print Page




Adobe ColdFusion 8.0.1 Update - Helping Programmers To Be Signifanctly Less Girlie - Download ColdFusion 8 Update 8.0.1 Now.

Reader Comments

well done. Very neat coding. It's not that kinky at all. I will give 10/10. :)

Posted by Make Money Online on Jul 17, 2007 at 10:31 AM


Thanks dude.

I'll be looking forward to reading your blog. Who doesn't want to make free money online :)

Posted by Ben Nadel on Jul 17, 2007 at 10:46 AM


Ben,

How can we change change this to allow multiple file selection for the upload part? I am trying to create a library application where the web user can upload files to a temp directory. Just like you have done here :), but only allow them to make multiple selection and the ext would be a zip file only as an example.

This way the end user would not have to enter all the contact info multiple times.

Thanks

Jim

BTW I am learning a lot from your blog. Thanks for putting this togethor

Posted by Jim Schell on Jul 17, 2007 at 10:57 AM


@Jim,

What you could do is just name the file field with some sort of numeric suffix:

name="file1"
name="file2"
name="file3"

Then, basically the validation process would just loop over those. I will see if I can get a demo together for that sort of thing. Since this gets more complex, it's probably better to start abstracting out some of these functions.

Posted by Ben Nadel on Jul 17, 2007 at 11:06 AM


Also, if you're specifying multiple file upload fields you're always guessing at the correct number of fields to offer your users. Is 4 fields too many or too few? To get around that issue, you can use GMail's technique of dynamically creating upload fields as the user needs them, as Pete Freitag mentions in his blog:
http://www.petefreitag.com/item/587.cfm

Posted by Tom Mollerus on Jul 17, 2007 at 11:40 AM


@Tom,

Good link / advice. Thanks.

Posted by Ben Nadel on Jul 17, 2007 at 11:52 AM


Ben:

In the code below why do you need to delete the file if the <cfelseif ArrayLen( arrErrors )> is true? The way I read your code, the file is never actually uploaded to the server if there are any errors found in the form field values entered by the user.

<cfif NOT Len( FORM.resume )>

<cfset ArrayAppend(
arrErrors,
"Please select a resume to upload"
) />

<cfelseif ArrayLen( arrErrors )>

<!---
The file has been selected, but there are errors
caused by other parts of the form validation.
Therefore, we now have a file that is just
sitting in our temp folder. Delete this file to
keep a clean server.
--->
<cftry>
<cffile
action="DELETE"
file="#FORM.resume#"
/>

<cfcatch>
<!--- File delete error. --->
<cflog date="yes" file="fileError" text="Had to delete #FORM.resume# since there were other errors in the form." type="information" time="yes">
</cfcatch>
</cftry>

<cfelse>

Posted by Bruce on Jul 17, 2007 at 12:23 PM


@Bruce,

The file is always uploaded to the server (from my understanding), but it starts out by being uploaded to the server's temp directory. If you try to dump out the FORM.resume value, you will see that it points to a .TMP file on the server. The CFFILE tag is not actually uploading the file - it's really just moving it from the temp directory to the directory you are choosing (and taking care of naming conflicts, modes, and client/server name values).

But, like I said, this is probably overkill. I put it more in there to get people thinking - I never actually delete that. I am not sure how often the temp directory gets cleaned, or if it cleans on server-restart. I just wanted to get people thinking. If you remove it, unless you are doing TONS of file uploads, I doubt it will be an issue.

Of course, I could also be wrong. However, I can tell you that the CFFILE delete there doesn't throw an error, so the file must exist.

Posted by Ben Nadel on Jul 17, 2007 at 12:30 PM


Ben:

I don't think the file is actually uploaded to the server until the cffile upload is executed. I did a test of commenting out the cffile delete in that cfelseif block and instead just logged the form.resume value.

I reloaded the page, filled in the form with a bad email address, but selected a valid file. I clicked on the Upload Resume button. I get the error message about the bad email.

When I go to the directory on my server where the form.resume value (that was written to my log file) is supposed to be located, nothing is there, even though I commented out the cffile delete.

Bruce

Posted by Bruce on Jul 17, 2007 at 1:16 PM


@Bruce,

I am sure that ColdFusion is probably handling this elegantly in some way. However, if I put this right before the CFFILE[ Action = Delete ]:

<cfdump var="#FileExists( FORM.resume )#" />
<cfabort />

It outputs:

YES

So, the file is definitely there at the time the code runs. Now, maybe if you don't use the file, ColdFusion deletes is automatically or something? Not sure.

Posted by Ben Nadel on Jul 17, 2007 at 1:43 PM


Hi Ben -

I am using some of this code for a presenter form on my site. Like Jim, and me being a newb, I would like to have multiple file uploads. Could you elaborate a bit further on your response to Jim? I am trying to get users to upload a resume and a presentation file, but only the alst file to be upload gets submitted with the results.

Thanks,
Adam

Posted by Adam on Dec 4, 2007 at 1:50 PM


Hey Guys, I am using the Cf_Fileupload tag for multiple file upload but the cause is the same it just uploads the single last file, not the previous one..

I think the only thing cause it to get the last file is this attributee of:

<cfset strFilePath = (
CFFILE.ServerDirectory & "\" &
CFFILE.ServerFile
) />

Posted by Mat on Feb 15, 2008 at 12:10 AM


That's a really nice bit of tight and thoughtful code. Well done. Was a joy to read, and easy to follow. Good job.

Posted by Frank Marion on Mar 30, 2008 at 11:41 PM


Create artcile, thanks!

Just curious how you handle the catch-22 of invalid file names. The user has a file with characters that aren't liked by cfmailparam such as "this+that.doc"

Typically, when I receive an uploaded file, I change the name to the ID number of the database record that holds the information about that file such as 123.doc. That ensure it's valid and when downloading back to the user, I use cfheader to present it with it's original name.

But with CFMAILPARAM, there is no such option to store with one name and send with a different name. So you either have to give the user a meaningless file name like '123.doc' or you have to strip out invalid characters and give them a "modified" file name, which is almost as bad.

Any thoughts? :)

Posted by Gary on Jul 18, 2008 at 9:48 AM


@Gary,

That is an interesting question. To be honest, I have not run up against an invalid file name in the CFMailParam tag before; before this, I am not sure I even suspected that that was possible. I will look into this to see if I can come up with anything.

Posted by Ben Nadel on Jul 18, 2008 at 9:58 AM


Post Comment  |  Ask Ben


Home   |   Web Log   |   ColdFusion   |   Projects   |   Resume   |   Job Form   |   Search   |   Contact
Epicenter Consulting - Custom Software Solutions for Business Evolution HostMySite.com - The Leader In ColdFusion Hosting