Generic Form Processing / Emailing In ColdFusion
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
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.
@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!
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 :(
@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?
@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.
@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!
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 :)
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
@Mark,
I saw your post earlier. Looks good to me :)
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
@JJ,
Glad this helped in some way.
It doesn't seem like it handles check boxes that are not checked (no value).
If it does, please let me know.
@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.
How could you limit the size of the file when it's being attached to the email.
@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.
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.
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?
@Mike,
You mean that ColdFusion cannot upload files that have a "#" their file name?
Disregard post, our it works on our dev server, so its server side on my end.
@Mike,
No problem.