In the first part of this article, we covered some general principles for handling an email blast from ColdFusion. In this part, I'll present some code that should make it very easy to set up a recurring mailing for a newsletter. Community MX uses a similar system for sending out the weekly newsletter.
The first thing you'll need to do is to make sure your CF server and mail server are set up be able to handle a blast of several hundred or several thousand messages. Trial and error will usually get you through this step. For example, the mail server that I use can handle 200 threads (incoming and outgoing). ColdFusion MX 6.1 is capable of sending many more than that, so a proper setting in the Mail Delivery Threads box in the ColdFusion Administrator is necessary. I tried with 100 threads to start with, but the CF server was still hitting the mail server too hard--many emails were being sent to the undeliverable folder. This is because emails are being set out from the mail server at the same time that you are delivering emails to the mail server from ColdFusion. This results in an overloading of the mail server threads.
Also, I found out that the spool interval setting was too low as well, resulting in the CF server attempting to blast the emails faster than the mail server could handle. I increased that setting to 10 seconds to give the mail server room to breathe.
When I reduced the thread count to 50, I was still getting a few emails in the undeliverable folder. I set it down to 25, and everything now runs smoothly.
Next, we'll have to set up the directory for the email blast. It is a good idea to place the email application within a protected directory. The easiest way to protect a directory in ColdFusion is to create an Application.cfm file and place some code in their to protect files if you aren't logged in. You can find information on setting up a login system on Community MX at http://www.communitymx.com/content/article.cfm?cid=A9FD681865591F12.
The directory that you use for the email blast will need to have two subdirectories: reports and emailbody. The reports directory will be a repository for reports each time you make an email blast. The emailbody directory is where you will upload your messages to be sent.
If your servers are configured and directories created, you can create the pages necessary to send the emails. This can be done with one page, although I prefer to create a separate emailform.cfm page and sendemail.cfm page. That way I can keep the email functionality separated from the form.
There are several ways to approach an email form page to send out a newsletter or other customer communication or email blast. One way is to use form fields to paste the contents of the message into when sending the blast. The disadvantage of this method is that you have to paste the message into the form each time you send the email.
The form below uses another approach: uploading the email text as a file, so that it will be in place on the server when you are sending the blast. The form also has several other features for making the email blast easy:
The form is below in Listing 1. An explanation follows:
<html>
<head>
<title>Input Form</title>
<style type="text/css">
<!--
body, td, th, input {font-family: Verdana, Arial, Helvetica, sans-serif; font-size:12px;}
.longTextField {width: 350px; font-size: 12px}
.shortTextField {width: 80px; font-size: 12px}
h1 {font-size: 24px;}
h2 {font-size: 16px;}
hr { width:80%; height:1px; color:#660033;align:center;}
th {text-align:right;}
-->
</style>
<script language="JavaScript" type="text/javascript">
<!--
function MM_findObj(n, d) { //v4.01
var p,i,x; if(!d) d=document; if((p=n.indexOf("?"))>0&&parent.frames.length) {
d=parent.frames[n.substring(p+1)].document; n=n.substring(0,p);}
if(!(x=d[n])&&d.all) x=d.all[n]; for (i=0;!x&&i<d.forms.length;i++) x=d.forms[i][n];
for(i=0;!x&&d.layers&&i<d.layers.length;i++) x=MM_findObj(n,d.layers[i].document);
if(!x && d.getElementById) x=d.getElementById(n); return x;
}
//-->
</script>
</head>
<body onload="MM_findObj('sendTo').value=MM_findObj('getNames').options[MM_findObj('getNames').selectedIndex].value">
<h1>Send the Newsletter </h1>
<h2>Step 1:</h2>
<p>Upload your text AND html version newsletters. If newsletter already uploaded, go to next step
<form name="upload" action="emailform.cfm" enctype="multipart/form-data" method="post">
<input name="filefield" type="file" class="longTextField">
<input name="Submit" type="submit" class="shortTextField" value="Submit">
</form>
<cfif isdefined ("form.filefield")>
<cffile action="upload"
destination="#GetDirectoryFromPath(GetTemplatePath())#emailbody/"
filefield="filefield">
</cfif>
<h2>Step 2:</h2>
<p>Test your email before sending. Choose the correct files and the correct address to send to. <em>Start Row</em> and <em>End Row</em> should both be set to "1". </p>
<h2>Step 3:</h2>
<p>After testing, choose to send to ALL. Change <em>End Row</em> to "end". This sends the newsletter to EVERYONE IN THE DATABASE. </p>
<form name="form1" action="sendemail.cfm" method="post">
<table>
<tr>
<td colspan="3"><hr></td>
</tr>
<tr>
<th id="select"> Select The HTML Newsletter:</th>
<td> </td>
<td><cfdirectory name="rsDirectory"
action="list"
directory="#GetDirectoryFromPath(GetTemplatePath())#emailbody/">
<select name="htmlversion" class="longTextField" id="htmlversion">
<option value="--Pick a file --">--Pick a file for HTML Version--</option>
<cfoutput query="rsDirectory">
<option value="#rsDirectory.name#">#rsDirectory.name#</option></cfoutput>
</select> </td>
</tr>
<tr>
<th id="selecttext"> Select The TEXT version Newsletter:</th>
<td> </td>
<td>
<select name="textversion" class="longTextField" id="textversion">
<option value="--Pick a file --">--Pick a file for Text Version--</option>
<cfoutput query="rsDirectory">
<option value="#rsDirectory.name#">#rsDirectory.name#</option></cfoutput>
</select> </td>
</tr>
<tr>
<th id="helpSubject">Subject for email </th>
<td> </td>
<td><input name="subject" type="text" class="longTextField" id="subject" value="Here is the latest news from Community MX"></td>
</tr>
<tr>
<th id="text"> Test TEXT version email:</th>
<td> </td>
<td><input name="textonly" type="Checkbox" id="textonly" value="T">
(for testing only)</td>
</tr>
<tr>
<th colspan="3"><hr></th>
</tr>
<tr>
<th id="addresses">Send to:</th>
<td> </td>
<td>
<select name="getNames" class="longTextField" onchange="document.forms[1].sendTo.value=this.options[this.selectedIndex].value">
<option value="tommuck@basic-drumbeat.com">Send mail to TOM</option>
<option value="tommuck@hotmail.com">Send mail to TOM hotmail</option>
<option value="?">Other</option>
<option value="ALL">Sending Actual job</option>
</select> </td>
</tr>
<tr>
<th id="sendto"> (Sending to)</th>
<td> </td>
<td><input type="text" name="sendTo" class="longTextField"></td>
</tr>
<tr>
<th colspan="3"><hr></th>
</tr>
<tr>
<th id="startrow"> Start Row:</th>
<td> </td>
<td> <input type="text" name="startRow" value="1" class="shortTextField"> </td>
</tr>
<tr>
<th id="endrow"> End Row:</th>
<td> </td>
<td> <input type="text" name="endRow" value="1" class="shortTextField">
(for final job write "end") </td>
</tr>
<tr>
<th id="col021"> START FROM Record Number:</th>
<td> </td>
<td> <input name="startfrom" type="text" class="shortTextField" id="startfrom" value="1">
(set to 1. In the event of an error, set to record number) </td>
</tr>
<tr>
<th colspan="3"><hr></th>
</tr>
<tr>
<th id="report"> Enter the email addresses of people to send report to:</th>
<td> </td>
<td> <input type="text" name="reportMail" size="30" value="<cfif isdefined("cookie.report")><cfoutput>#cookie.report#</cfoutput></cfif>" class="longTextField"> </td>
</tr>
<tr>
<th> </th>
<td> </td>
<td><input Name="submitForm" type="Submit" class="shortTextField" value="Submit"></td>
</tr>
</table>
</form>
</body>
</html>
Listing 1: The emailform.cfm page
The resulting form looks like this:
Figure 1: The emailform.cfm file produces this HTML form to send the email blast
The code contains two forms. The first form (in Step 1) allows you to upload your email text -- HTML and text versions of the email you want to send. The second form contains the fields used to send the email:
Most of the code is self-explanatory. Following are a few of the tricky areas:
<cfif isdefined ("form.filefield")>
<cffile action="upload"
destination="#GetDirectoryFromPath(GetTemplatePath())#emailbody/"
filefield="filefield">
</cfif>
This section reads the file specified in the form.filefield field from the clients machine and uploads it to the server, storing the results in the emailbody directory.
<cfdirectory name="rsDirectory"
action="list"
directory="#GetDirectoryFromPath(GetTemplatePath())#emailbody/"> <select name="htmlversion" class="longTextField" id="htmlversion">
<option value="--Pick a file --">--Pick a file for HTML Version--</option>
<cfoutput query="rsDirectory">
<option value="#rsDirectory.name#">#rsDirectory.name#</option>
</cfoutput>
</select>
This section reads the directory on the server using the current path of the emailform.cfm page, and browsing the emailbody directory within it. A <select> field is then set up to dynamically display the filenames in this directory by using a <cfoutput> to loop over the query returned from <cfdirectory>.
<select name="getNames" class="longTextField" onchange="document.forms[1].sendTo.value=this.options[this.selectedIndex].value">
<option value="tommuck@basic-drumbeat.com">Send mail to TOM</option>
<option value="tommuck@hotmail.com">Send mail to TOM hotmail</option>
<option value="admin@jumpinjehosephat.com,tom@hotmail.com,tommuck@aol.com">Send mail client test list </option>
<option value="?">Other</option>
<option value="ALL">Sending Actual job</option>
</select>
This dropdown list supplies test email addresses. You can put as many addresses as you want in this list for testing, and even include lists of people separated by commas. This is handy for testing newsletters for clients, or for testing to multiple accounts, such as Hotmail or AOL. The JavaScript function in the onchange event will fill in a plain text field with the addresses. The text field allows you to modify or fill in your own addresses, and is the field that supplies the <cfmail> tag on the second page with the TO address.
The last option is to send to ALL addresses. At this point, testing is over and the <cfmail> tag will use actual email addresses from your database.
For the sake of the article, I'm assuming that you have a database that you want to use for an email blast. The database should have a table that stores information about users or customers. The two required fields in this table are email address and identifier. In the code that follows, we'll be using a table named Customers with fields named email and CustomerID.
The reason we need the identifier is that we will be storing the number in a text file on disc. Because of the nature of the beast (mass emails), there are many times when a mail delivery can error. For example, if you have bad email addresses stored in the email field, there could be an error. The server could fail in the middle of the blast. In any event, as a safeguard we will store the current record number in a text file so that if a problem occurs, we will know where the blast should be picked up from.
The database table used for the sample is shown below:
CustomerID | Name | |
---|---|---|
1 | Binky Tooskinny | binky@binkysgym.com |
2 | Joe Schmoe | jschmoe@schmoeenterprises.com |
3 | John Jehosephat | jj@jehosephatlodge.com |
The table structure is as follows:
CREATE TABLE Customers (
CustomerID int IDENTITY (1, 1) NOT NULL ,
Name varchar (128) NULL ,
Email varchar (128) NULL
)
With the form page under our belt and the data in place, it's time to concentrate on the action page. This is where the <cfmail> tag will go, and also the query -- or queries -- that drive the email. Following is sendemail.cfm. The code is commented inline:
<cfprocessingdirective suppresswhitespace="Yes">
<cfcookie name="report" value="#form.reportmail#" expires="NEVER">
<cfparam name="form.startfrom" default=1>
<!--- Set up a test flag --->
<cfparam name="test" default="true">
<!--- Set up Variables for Counters --->
<cfset startRow=#trim(form.startRow)#>
<cfset endRow=#trim(form.endRow)#>
<!--- Set up variables for reporting --->
<cfset startTime=Now()>
<!--- Set up variable for page path --->
<cfset thePath = #GetDirectoryFromPath(GetTemplatePath())#>
<!--- Set up carriage return variables for emails --->
<cfset crlf=chr(13) & chr(10)>
<!---This rsGetAllEmails query is to get all customer info--->
<cfquery name="rsGetAllEmails" datasource="test">
SELECT * FROM Customers ORDER BY CustomerID
</cfquery>
<!--- Set up the TO field for the email based on form field --->
<cfset variables.tofield = form.sendTo>
<!--- If sending all messages, set the counter for the loop --->
<cfif endRow EQ "end">
<cfset endRow=#rsGetAllEmails.RecordCount#>
</cfif>
<cfset emailCounter = 0>
<!--- Create a report filename to use --->
<cfset reportFileName = "#thePath#reports/currentRow#dateformat(now(),'ddmmyyyy')##timeformat(now(),'hhmm')#.txt">
<!--- Begin the loop for the emails --->
<cfloop query="rsGetAllEmails" startrow=#startRow# endrow=#endRow#>
<cfset emailCounter = emailCounter + 1>
<!--- These lines set up the actual email address in the job when not testing --->
<cfif form.sendTo EQ "ALL">
<cfset variables.toField=#rsGetAllEmails.email#>
<cfset test="">
</cfif>
<!--- set up a text file so that the current row number is always written
to the file --->
<cffile action="WRITE"
file="#reportFileName#"
output="#rsGetAllEmails.customerid#">
<cfif isdefined("form.textonly")>
<!---Send a sample text version --->
<cfmail to='#variables.tofield#'
from='"Community MX" <info@communitymx.com>'
replyto="info@communitymx.com"
subject="#form.subject#"
failto="bounce@communitymx.com"
type="text/plain">
<cfinclude template="emailbody/#form.textversion#">
</cfmail>
<cfelse>
<!---send the multipart email--->
<cfmail to='#variables.tofield#'
from='"Community MX" <info@communitymx.com>'
replyto="info@communitymx.com"
subject="#form.subject#"
failto="bounce@communitymx.com">
<cfmailpart type="text/plain">
<cfinclude template="emailbody/#form.textversion#">
</cfmailpart>
<cfmailpart type="text/html">
<cfinclude template="emailbody/#form.htmlversion#">
</cfmailpart>
</cfmail>
</cfif>
</cfloop>
<cfset endTime=Now()>
<cfset timeToSend = DateDiff("s",startTime,endTime)>
<!--- Set up the variable for the report body for the text file --->
<cfset variables.theReport = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<style>
<!--
p {font-family : Verdana, Geneva, Arial, Helvetica, sans-serif;font-size:12px;}
-->
</style>
</head>
<body>
<p>Sending Community MX Sample on #dateformat(now())#</p>
<p>Total Emails Addresses Received: #rsGetAllEmails.RecordCount# </p>
<p>Total Emails Sent: #emailCounter#</p>
<p>Date job was sent out: #DateFormat(Now(), "dd mmm yyyy")#<br>
Time job was sent out: #TimeFormat(startTime,"hh:mm:ss tt")#</p>
<p>Time to send job: #timeToSend# seconds</p>
</body>
</html>'>
<!--- Send an email report to the people listed in the form page --->
<cfmail to="#form.reportMail#"
from="info@communitymx.com"
subject="Report for Community MX"
Type="html">
#variables.theReport#
</cfmail>
<cfif test EQ "">
<!--- Write the report to disk --->
<cffile action="WRITE" file="#thePath#reports/report#dateformat(now(),'ddmmyyyy')##timeformat(now(),'hhmmss')#.cfm" output=#variables.theReport#>
</cfif>
</cfprocessingdirective>
<!--- Display the report on the page too! --->
<cfoutput>#variables.theReport#</cfoutput>
The email bodies are included using <cfinclude>. When using this technique, remember that you have to include <cfoutput></cfoutput> around any variables in your text.
There are several steps to sending the email job. They are as follows:
The email job may take a few minutes to send, depending on how many addresses you are sending to. Leave the browser open for this time. Typically, you can send about 1000 per minute or more. The report is written to disk in the reports folder with a name like report301220030311.cfm and the file that handles the current record number is written to disc with a name like currentRow_301220030311.txt. Also, the
ColdFusion 6.1 has made it very easy to create professional looking email blasts by using the new multipart functionality. This article has shown a simple form-based approach to sending an email blast. Part 3 will show a more complex job that includes line items.