Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: James Padolsey
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: James Padolsey ( @padolsey )

Generic Form Processing / Emailing In ColdFusion

By on
Tags:

The other day, we had a client that told us they were switching from ColdFusion to PERL. Naturally, this struck us as a totally insane move; higher level languages like ColdFusion were created so that people no longer had to touch things like PERL. The idea of going back to PERL would be like some reverse evolution. After discussing this move with the client, trying to find the root cause of this move, we discovered that it had nothing to do with ColdFusion or PERL specifically. Rather, the client wanted to move to PERL to utilize the generic form handler in Matt's Script Archive. That's it, that was the executive decision to switch programming languages.

Since ColdFusion is awesome and I want our clients to stay on it, I decided to go ahead and build a generic form handler for public consumption. It probably doesn't have all the functionality that Matt's Script Archive generic form handler has, but I think it provides a good deal of functionality with almost no effort. You define the actions of the form processing by providing the following hidden fields:

to_email - This is the email address to which the email will be sent.

from_email - This is the email address from which the email will be sent. If this field is not included, the to_email address will automatically be used for both the to and from field.

subject - The subject text of the outgoing email.

success_url - This is the url to which we should forward the page control once the email has been sent out. If this is not included, the processing page will provide it's own generic success page. This URL must either be an absolute url or one that is relative to the generic form processing template since it will have to perform a relative CFLocation tag.

temp_directory - This is the full path directory in which uploaded files will be stored so that they can be attached to the outgoing email. If you are going to use this, remember to set your FORM encryption type to: enctype="multipart/form-data"; otherwise, the files will not submit as uploaded files. If you don't want to put the temp file path in the form, you can define it in the processing page for the same effect (and public users won't get any insight into your server's directory structure). If you do not include this value, then the uploaded files will not be attached to the outgoing email (and the file fields will be put in the email body).

field_list - This is the comma-delimited list of fields to include in the outgoing email. This is there purely to allow users to provide an explicit field order for the outgoing email. If you do not include this value, then all fields will be included in an arbitrary order (what ever StructKeyList() returns). If you do include this field, only those defined fields will be used.

Implementing the ColdFusion generic form processing is insanely simple. Here is a test page that demonstrates how the hidden fields should be included in the form:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<title>Generic Form Processing Test</title>
</head>
<body>

	<cfoutput>

		<form
			action="./process_email.cfm"
			method="post"
			enctype="multipart/form-data">

			<!--- Hidden meta-data values. --->
			<input type="hidden" name="to_email" value="stacy@girls-who-code.com" />
			<input type="hidden" name="from_email" value="kim@girls-who-code.com" />
			<input type="hidden" name="subject" value="Generic Email Processing Test Email" />
			<input type="hidden" name="success_url" value="process_email_success.cfm" />
			<input type="hidden" name="temp_directory" value="#GetDirectoryFromPath( GetCurrentTemplatePath() )#files\" />
			<input type="hidden" name="field_list" value="FirstName, LastName, ContactType, Comments" />

			<dl>
				<dt>
					First Name:
				</dt>
				<dd>
					<input type="text" name="FirstName" value="" />
				</dd>

				<dt>
					Last Name:
				</dt>
				<dd>
					<input type="text" name="LastName" value="" />
				</dd>

				<dt>
					Contact Type:
				</dt>
				<dd>
					<input type="radio" name="ContactType" value="Friend" /> Friend<br />
					<input type="radio" name="ContactType" value="Co-Worker" /> Co-Worker<br />
				</dd>

				<dt>
					Comment:
				</dt>
				<dd>
					<textarea name="Comments" rows="5" cols="40"></textarea>
				</dd>

				<dt>
					Resume:
				</dt>
				<dd>
					<input type="file" name="resume" />
				</dd>

				<dt>
					Additional Doc.:
				</dt>
				<dd>
					<input type="file" name="add_doc" />
				</dd>

			</dl>

			<input type="submit" value="Test Email Processing" />

		</form>

	</cfoutput>

</body>
</html>

As you can see, the hidden form fields are just provided as the first elements of the form. Notice also that the form Method points directly to our generic form processor. There is a certain amount of exposure here since the email addresses and the directory structure are out in the open, for any person or bot that knows how to view page source. If you don't want to put those values there, you can always move them directly into the ColdFusion CFParam tags at the top of the email processing page, which is more secure, but also, less flexible.

Here is the ColdFusion page that handles the generic form processing:

<!---
	The email address to which the email will be sent. If you
	don't want to put email addresses in the public form, you
	can always put it in the Default attribute of this tag.
--->
<cfparam
	name="FORM.to_email"
	type="string"
	/>

<!---
	The email address from which the email will be sent. If
	this field is not included, the to_email address will
	automatically be used for both the to and from field.
--->
<cfparam
	name="FORM.from_email"
	type="string"
	default="#FORM.to_email#"
	/>

<!--- The subject text of the outgoing email. --->
<cfparam
	name="FORM.subject"
	type="string"
	/>

<!---
	The url to which we should forward the page control once
	the email has been sent out. If this is not included, the
	page will just close out when the processing is done.
	NOTE: This URL must either be an absolute url or one that
	is relative to THIS PROCESSING TEMPLATE.
--->
<cfparam
	name="FORM.success_url"
	type="string"
	default=""
	/>

<!---
	The full path directory in which uploaded files will be
	stored so that they can be attached to the outgoing email.
	If you are going to use this, remember to set your FORM
	encryption type to: enctype="multipart/form-data";
	otherwise, the files will not submit as uploaded files.

	If you don't want to put the temp file path in the form,
	you can put it in the default value here. That way, no
	public user would see anything about your server's
	directory structure.
--->
<cfparam
	name="FORM.temp_directory"
	type="string"
	default=""
	/>

<!---
	This is the comma-delimited list of fields to include in
	the outgoing email. If you do not include this value, then
	all fields will be included in an arbitrary order.
--->
<cfparam
	name="FORM.field_list"
	type="string"
	default=""
	/>


<!---
	ASSERT: At this point, we have paramed the data in the
	FORM scope. Any data that was required but did not exist
	will have thrown an error.
--->


<!---
	Copy the FORM data into the another structure so that we
	can remove the special Email-related data keys without
	echoing them in the email body.
--->
<cfset REQUEST.EmailData = Duplicate( FORM ) />

<!--- Delete the email meta data. --->
<cfset StructDelete( REQUEST.EmailData, "fieldnames" ) />
<cfset StructDelete( REQUEST.EmailData, "to_email" ) />
<cfset StructDelete( REQUEST.EmailData, "from_email" ) />
<cfset StructDelete( REQUEST.EmailData, "subject" ) />
<cfset StructDelete( REQUEST.EmailData, "success_url" ) />
<cfset StructDelete( REQUEST.EmailData, "temp_directory" ) />
<cfset StructDelete( REQUEST.EmailData, "field_list" ) />


<!---
	Build an array to hold the form fields that might have
	uploaded files. This will be translated into future
	CFMailParam tags.
--->
<cfset REQUEST.Files = ArrayNew( 1 ) />

<!---
	Now that we have a clean EmailData structure, we want to
	see if the user has any files that they are uploading.
	To do this, we are going to see if any of the form values
	translates to a temporary file (which is how ColdFusion
	displays uploaded files - the form field value is not the
	uploaded file, but rather the temp file on the server).
--->
<cfif Len( FORM.temp_directory )>

	<!--- Loop over all the form fields. --->
	<cfloop
		item="REQUEST.Key"
		collection="#REQUEST.EmailData#">

		<!---
			Check to see if this form field has a temp file.
			This will be a value that starts with a drive letter
			such as D: and ends with a .tmp file extension.
		--->
		<cfif REFind( "^\w:.+?\.tmp$", REQUEST.EmailData[ REQUEST.Key ] )>

			<!---
				This form field holds the temporary path of an
				uploaded file. We don't want this to be part of
				the primary form fields so, remove this key from
				the form data.
			--->
			<cfset StructDelete( REQUEST.EmailData, REQUEST.Key ) />

			<!---
				Upload the file to the user-defined temp
				directory. This is really just moving it
				from ColdFusion's temp directory to our
				temp directory and renaming it.
			--->
			<cffile
				action="upload"
				destination="#FORM.temp_directory#"
				filefield="#REQUEST.Key#"
				nameconflict="makeunique"
				/>

			<!---
				Now that the file has been uploaded, add the
				target file path to the array of files that will
				be attached to the outgoing email.
			--->
			<cfset ArrayAppend(
				REQUEST.Files,
				"#CFFILE.ServerDirectory#/#CFFILE.ServerFile#"
				) />

		</cfif>

	</cfloop>

</cfif>


<!---
	Check to see if the user had defined the list of fields to
	use. If not, then just get the list of fields from the data
	struct (which will be returned in some arbitrary order).
--->
<cfif NOT Len( FORM.field_list )>

	<!--- Get key list for fields. --->
	<cfset FORM.field_list = StructKeyList( REQUEST.EmailData ) />

</cfif>


<!---
	Since this is a generic form handler, we don't have very
	much formatting. That gives us the ability to easily create
	both types of mail body - HTML and Plain Text.
--->


<!--- Build the HTML version of the email. --->
<cfsavecontent variable="REQUEST.HtmlBody">
	<cfoutput>

		<p>
			Email processed on
			#DateFormat( Now(), "mmm d, yyyy" )# at
			#TimeFormat( Now(), "hh:mm TT" )#
		</p>

		<h3>
			Form Data
		</h3>

		<dl>
			<!--- Loop over the form data. --->
			<cfloop
				index="REQUEST.Key"
				list="#FORM.field_list#"
				delimiters=",">

				<dt>
					#REQUEST.Key#
				</dt>
				<dd>
					#HtmlEditFormat(
						REQUEST.EmailData[ Trim( REQUEST.Key ) ]
						)#
				</dd>

			</cfloop>
		</dl>


		<!---
			Add some additional data about the user and where
			the form came from. This might help with debugging.
		--->

		<h3>
			User Data
		</h3>

		<dl>
			<dt>
				Referrer:
			</dt>
			<dd>
				#CGI.http_referer#
			</dd>
			<dt>
				IP:
			</dt>
			<dd>
				#CGI.remote_addr#
			</dd>
		</dl>

	</cfoutput>
</cfsavecontent>


<!---
	Build the Text version of the email. Don't worry about the
	leading white spaces in each line. We are going to trim
	that out afterwards (we are putting it in now so that our
	code page has nice flow to it).
--->
<cfsavecontent variable="REQUEST.TextBody">
	<cfoutput>

		Email processed on #DateFormat( Now(), "mmm d, yyyy" )# at #TimeFormat( Now(), "hh:mm TT" )#

		-- FORM DATA --

		<!--- Loop over the form data. --->
		<cfloop
			index="REQUEST.Key"
			list="#FORM.field_list#"
			delimiters=",">
			#Trim( REQUEST.Key )#
			#HtmlEditFormat( REQUEST.EmailData[ Trim( REQUEST.Key ) ] )#
		</cfloop>


		-- USER DATA --

		Referrer:
		#CGI.http_referer#
		IP:
		#CGI.remote_addr#

	</cfoutput>
</cfsavecontent>

<!---
	Because plain text emails keep all the white space, we
	have to remove the leading tabs from each line of the above
	content. This can be done with an easy multiline regex.
--->
<cfset REQUEST.TextBody = REReplace(
	REQUEST.TextBody,
	"(?mi)^\t+|\t+$",
	"",
	"all"
	) />


<!---
	Send out the email. When using both the plain text and the
	html mail parts, be sure to use the HTML version LAST. If
	you put it first, Google Mail will not render the email.
--->
<cfmail
	to="#FORM.to_email#"
	from="#FORM.from_email#"
	subject="#FORM.subject#">

	<!--- Include the Text version. --->
	<cfmailpart type="text/plain">#REQUEST.TextBody#</cfmailpart>

	<!--- Include the HTML version. --->
	<cfmailpart type="text/html">#REQUEST.HtmlBody#</cfmailpart>

	<!--- Loop over files that we need to attach. --->
	<cfloop
		index="REQUEST.FileIndex"
		from="1"
		to="#ArrayLen( REQUEST.Files )#"
		step="1">

		<!--- Attach file. --->
		<cfmailparam
			file="#REQUEST.Files[ REQUEST.FileIndex ]#"
			disposition="attachment"
			/>

	</cfloop>
</cfmail>



<!---
	Now that the email has been sent out, check to see if we
	have a success url to which me need to forward the user.
--->
<cfif Len( FORM.success_url )>

	<!--- Forward user. --->
	<cflocation
		url="#FORM.success_url#"
		addtoken="false"
		/>

</cfif>


<!---
	ASSERT: If we have made it this far, then the user did
	not provide a success URL. Therefore, we need to provide
	our own simple success page.
--->


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
	<title>Your Form Has Been Processed</title>
</head>
<body>

	<h1>
		Your Form Has Been Processed
	</h1>

	<p>
		Thank you for taking the time to fill out our form.
		It has been processed and sent to the appropriate
		people.
	</p>

	<p>
		<cfoutput>
			<a href="#CGI.http_referer#">Go back</a>
		</cfoutput>
	</p>

</body>
</html>

Now, one thing to realize is that the files that get uploaded and attached to the outgoing email don't get deleted from the server. We can't delete them after we send the email since emails don't go out directly. Yes, we could turn off spooling and send the email out directly and then delete the files, but I always hesitate to do that as I feel that it is forcing the eMail server to go against what it wants to do. You can either leave those files on the server, or make sure that the folder they point to gets periodically cleaned out.

When the email goes out, it goes out with both an HTML version and a Text version for clients that cannot handle HTML.

I hope that helps anyone who was thinking of switching from ColdFusion back to PERL :)

Want to use code from this post? Check out the license.

Reader Comments

36 Comments

A quick note that the to_email should not be in the form as a hidden field but rather embedded in the action page. What happens is bots look for this, and once they find the form they use it as a mail relay to send their spam to other people. Gets your box on black hole lists, etc.

15,674 Comments

@Joshua,

Absolutely. You can move the to_email into the CFParam in the processing page. My only concern was that cuts of the flexibility a bit; however, less flexibility is better than being blacklisted!

10 Comments

I'm actually working on something semi related right now where I'm looking to insert contacts into a TO: field from a list of pre-defined contacts, kind of like in Mac Mail/gmail etc. You don't happen to have any tips on how to do this the slickest do you? I know it's going to get messy with a bucket of javascript to handle the type-ahead :(

15,674 Comments

@Sam,

I think all you would need is a ContactID select box like this:

<option value="1">Ben</option>
<option value="2">Kit</option>
<option value="3">Sherri</option>

Then, on the processing page, you just have a struct that associates the IDs to the user address:

<cfset objContacts = StructNew() />
<cfset objContacts[ 1 ] = "ben@xyz.com" />
<cfset objContacts[ 2 ] = "kit@xyz.com" />
<cfset objContacts[ 3 ] = "sherri@xyz.com" />

Then, just grab the contact based on the submitted ContactID?

28 Comments

@Ben,

On the subject of deleting file attachments, just to be clear, there is an attribute of cfmail called "spoolenable" which lets you enable or disable spooling on a case-by-case basis. You could set it to "no" for any email which had attachments, and leave it set to yes for all other emails, which would let you spool most of the time, but still delete attachment files in real time so that they don't accumulate on the server.

And as for whether not spooling your email is a bad practice, I think that it's ok as long as it's for just a few emails now and then. They just go into the email server's queue to wait their turn, anyway.

15,674 Comments

@Tom,

Good call on the conditionally spooling. That never occurred to me. I knew about the attribute, but I never thought of giving it a variable rather than a hard-coded yes/no. Sweet action!

10 Comments

I'm looking to actually include a type ahead, so where you type 'mi' and it brings up 'mike'. Hit enter or the comma and start typing another and so on and so on.

But if I can't figure it out, I'll probably end up going with a check box or something similar :)

2 Comments

Ben:

Thanks for the post. I too had the need to upgrade some forms to move away from using the formmail.pl script. I ended up create a fairly direct port of the perl script to try to mimic all the functionality to create a solution that could be quickly dropped in place for an existing form. Your posting reminded me that I never posted about it nor published it, so thanks for the reminder! I detail more about it on my blog entry:
http://www.mkville.com/blog/index.cfm/2008/2/4/Simple-Form-Processing-and-Emailing-with-CFFormMail

You can find the project posted over at RIA Forge for downloading:
http://cfformmail.riaforge.org/

Mark

2 Comments

I was looking up a amortization formula and ended up running across something i was working on. i am always amazed how that happens, anyway awsome stuff. just wanted to say thanks. j

15,674 Comments

@Dwayne,

I believe you are correct - it cannot know about them as nothing gets passed. What you would have to do is change the checkbox to a radio box and explicitly pass the yes / no value.

15,674 Comments

@Dwayne,

After the file gets uploaded with CFFile, you can check the size of the file using the FileSize property of the CFFILE scope (that gets created after a file upload). I am pretty sure this is the size in bytes. You could check that, and if it's too large, you could return with an error (and delete the file from the upload destination).

But, there is no way to do this pre-upload as far as I know.

3 Comments

Thanks Ben, I appreciate your response. I do want the file uploaded regardless of size, but only want to attach it to the email message if its under 10mb.

I'm trying to figure out how to do something like
#REQUEST.Files[ REQUEST.FileIndex].filesize#

This maybe something to consider adding to the script as there are size limits on most email systems and as it's currently written, the email does not reach the recipient if the file size is to large for the smtp server.

Great script by the way...I've used it for several sites now.

2 Comments

One thing I noticed and haven't been able to find a solution to is the inability to upload office 2007 files. I spoke with another developer and he had a conflict with using "#" in the cffile filefield that was causing it not to work. Any idea on what conflict could possibly cause this not to upload office 2007 files?

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel