CFMail Best-Practices for Sending Web-Generated E-mail (e-cards, forward-to-a-friend, etc.)
There are a number of ColdFusion articles available addressing CFMail being detected as spam, etc. which primarily deal with the "Message-ID" contained in an email's header. Charlie Arehart has a couple of detailed examples on his blog which I recommend other ColdFusion developers consider reading:
- CF8 Hidden Gem: CFMAIL auto-generated message-id uses specified mail server name
- CFMAIL being detected as spam? Some solutions for CF 6, 7, and 8
While we will also be dealing with email headers, this article is primarily concerned with addressing suggestions brought forth by the Sender Policy Framework (SPF) and their article titled How web-generated e-mailers can avoid looking like forgers. In addition, the vast majority of projects I'm involved with are hosted in a shared-hosting environment. So we won't be modifying anything in the CFAdmin area.
If you're wondering how to view email headers, I found a pretty good link on the University of Delaware Police Computer Forensics Lab web site (I know, sounds pretty serious, doesn't it).
The code examples I use here have been tested on ColdFusion 8.0.1. However, these should work with CF 6, 7 and 8.0.0 with very little, if any, modifications.
To send a web-generated email, you should collect at a minimum the "From" email, the "To" email, a "Message," and preferably the senders "Name." Obviously, there are several other things you could collect such as "Subject" and who to "Cc" or "Bcc," but let's just keep this simple for now. So let's create a form to send a simple message using the following code:
<div style="padding:20px; background-color:#CCCCCC; border:1px solid #000000; width:325px;">
<cfform id="form1"
name="form1"
method="post"
action="#cgi.SCRIPT_NAME#?#cgi.QUERY_STRING#">
<p><label for="messageFromName">From Name:</label><br />
<cfinput type="text"
name="messageFromName"
id="messageFromName"
value="Suzie Visitor"
validate="noblanks"
size="30"
required="yes"
message="Name is required." /></p>
<p><label for="messageFromEmail">From Email:</label><br />
<cfinput type="text"
name="messageFromEmail"
id="messageFromEmail"
value="suzie.vistor@gmail.com"
validate="email"
size="30"
required="yes"
message="A valid email FROM is required." /></p>
<p><label for="messageTo">To:</label><br />
<cfinput type="text"
name="messageTo"
id="messageTo"
value="friend@theirdomain.com"
validate="email"
size="30"
required="yes"
message="A valid email TO is required." /></p>
<p><label for="messageText">Message:</label><br />
<cfinput type="text"
name="messageText"
id="messageText"
value="My simple message goes here."
validate="noblanks"
maxlength="160"
size="50"
required="yes"
message="Message is required." /></p>
<p><cfinput type="hidden" name="submitted" value="1" />
<cfinput type="submit"
name="submit"
value="Submit"
validate="submitonce" /></p>
</cfform>
<!--- place cursor in the first form element --->
<script type="text/javascript" language="JavaScript">
document.forms['form1'].elements['messageFromName'].focus();
</script>
</div>
Running the code should produce something similar to this:
The form doesn't do anything just yet other than post to the page/template it resides on. So let's go over the important stuff and then add some processing to the page.
According to SPF's article previously mentioned, when a web site sends web-generated e-mail, a user interacts with the web site, and an e-mail goes out on their behalf. Unfortunately, under SPF, mail from this type of service appears to be a forgery thereby being flagged as spam — unless certain precautions are taken. SPF claims that evite.com and egreetings.com have made the necessary changes and holds them up as examples other developers should emulate.
Your knee-jerk reaction to creating a CFMail to handle the form submission above might look like this:
<!---
in a shared-hosting environment, you'll need to supply "server"
and more than likely "username" and "password" too
--->
<cfmail to="#trim(FORM.messageTo)#"
from="#trim(FORM.messageFromName)# <#trim(FORM.messageFromEmail)#>"
server="mail.yourdomain.com"
username="info@yourdomain.com"
password="y0urP@55w0rdH3r3"
subject="Message from: #trim(FORM.messageFromName)#"
type="text"
charset="utf-8">
<cfmailpart type="text/plain" charset="utf-8">
#trim(FORM.messageText)#
</cfmailpart>
</cfmail>
However, this might (probably will) appear to be spam because it would generate something like this whilst using mail.yourdomain.com to send it:
Return-Path: suzie.visitor@gmail.com
From: "Suzie Visitor" <suzie.visitor@gmail.com>
Subject: Message from Suzie Visitor
SPF claims that "messages that use the user's address, but come from your mail servers are considered suspicious by SPF. To solve this problem, just change the headers ...
Here's a recommended example from SPF:
Return-Path: info@yourdomain.com
From: "Suzie Visitor" <info@yourdomain.com>
Reply-To: "Suzie Visitor" <suzie.visitor@gmail.com>
Subject: Message from Suzie Visitor
Keys to remember:
- Choose a general address from your domain (info@yourdomain.com)
- Add / Change the return-path (or "failto" in the CFMail tag) to that address
- Change the "From" header ("from" in the CFMail tag) to that same address
- Add a "Reply-To" header (yep, you guessed it, "replyto" in the CFMail tag) that contains your user's email address
So, to summarize, you can simply add the following code directly after the form and update the appropriate fields as necessary:
<cfparam name="REQUEST.errors" default="" />
<!--- PROCESS FORM SUBMISSION --->
<cfif isDefined("FORM.submitted") and FORM.submitted EQ 1>
<!--- create an implicit empty array to track form validation errors --->
<!--- if using CF 6 or 7, you will have to modify the following line by
changing the square brackets [] to ArrayNew(1) --->
<cfset REQUEST.errors = [] />
<!--- validate required form field entries --->
<cfif NOT len(trim(FORM.messageFromName))>
<cfset ArrayAppend(REQUEST.errors, "Name is required.") />
</cfif>
<cfif NOT isValid("email", trim(FORM.messageFromEmail))>
<cfset ArrayAppend(REQUEST.errors, "A valid email FROM is required.") />
</cfif>
<cfif NOT isValid("email", trim(FORM.messageTo))>
<cfset ArrayAppend(REQUEST.errors, "A valid email TO is required.") />
</cfif>
<cfif NOT len(trim(FORM.messageText))>
<cfset ArrayAppend(REQUEST.errors, "Message is required.") />
</cfif>
<!--- as long as there are NO errors, then process the contents --->
<cfif NOT ArrayLen(REQUEST.errors)>
<cftry>
<!--- send the email --->
<cfmail to="#trim(FORM.messageTo)#"
from="#trim(FORM.messageFromName)# <info@yourdomain.com>"
failto="info@yourdomain.com"
username="info@yourdomain.com"
password="y0urP@55w0rdH3r3"
replyto="#trim(FORM.messageFromName)# <#trim(FORM.messageFromEmail)#>"
server="mail.yourdomain.com"
subject="Message from: #trim(FORM.messageFromName)#"
type="text"
charset="utf-8">
<cfmailpart type="text/plain" charset="utf-8">
#trim(FORM.messageText)#
</cfmailpart>
</cfmail>
<cfcatch>
<cfoutput>#cfcatch.Message#</cfoutput>
</cfcatch>
</cftry>
<!--- show friendly message to submitter --->
<cfoutput>
<p>Your message has been sent at <em>#dateformat(now(), "long")# #timeformat(now(), "long")#</em> as follows:</p>
<p>TO: #FORM.messageTo#<br />
SUBJECT: Message from: #FORM.messageFromName#<br />
MESSAGE:<br /></p>
#htmleditformat(FORM.messageText)#
</cfoutput>
<cfelse>
<!--- if required form values weren't given, show message to submitter --->
<cfoutput>
<h4>Please review the following issues:</h4>
<ul>
<cfloop index="intError" from="1" to="#ArrayLen(REQUEST.errors)#" step="1">
<li>#REQUEST.errors[intError]#</li>
</cfloop>
</ul>
</cfoutput>
</cfif>
</cfif>
By following this example, you should be able to create some pretty interesting web-generated email applications that fit within the Sender Policy Framework guidelines.
If you have additional information, please feel free to share it! I hope this helps you in your next project.
Comments
from="#trim(FORM.messageFromName)# <info@yourdomain.com>"
...
server="mail.yourdomain.com"
We're using one email server for many different domains, so it would look more like:
from="#trim(FORM.messageFromName)# <info@theirdomain.com>"
...
server="mail.ourdomain.com"
Where 'theirdomain' is the domain where the clients web form is located and 'ourdomain' refers to the general smtp server we send all emails from. This doesn't seem correct in how we've set this up. Or will this work? Is there a way around this? Thanks.
Your current method could be flagged as potential spam because the from address does not match the actual sending domain (or server).
Actually, I've run into this myself on a past project and this is what we did:
failto:"info@actualMailServer.com"
server="mail.actualMailServer.com"
from="Client Name <info@actualMailServer.com>"
replyto="#trim(FORM.messageFromName)# <#trim(FORM.messageFromEmail)#>"
Be sure to include the "failto" because this represents the Return-Path in SPF. So, just some minor changes to your setup and you should be within the SPF recommendations.
Hope this helps!