Note: This script has been tested on a Windows 2000 box running ColdFusion versions 4.5 and 5.0. Unfortunately with no Solaris or Linux boxes around to test this, I can only assume that it works there as well. My cursory research shows that it *should* work. If this is not the case, please leave a comment below.

If you've used ColdFusion for any length of time, you've probably had to deal with undeliverable mail before. When you use the <CFMAIL> tag and it cannot successfully send the message to its destination, it deposits the message in the UNDELIVR directory (by default on Windows, is at C:\CFUSION\MAIL\UNDELIVR\).

Now, wouldn't it be great to do something with these e-mail bounces? Maybe you could remove a user from the database if the e-mail address is incorrect, send an e-mail to an administrator or simply delete the message. Well, that's what this script allows you to do.

The .cfmail File

Let's first look at the kind of .cfmail file that ColdFusion creates. Here is an example file. The line numbers have been added in, more on that later.

1: x-cf-version: 4.5.0

2: x-cf-server: mail.somewhere.com

3: x-cf-port: 25

4: x-cf-timeout: 60

5: x-cf-from: automated_beacon@somewhere.com

6: x-cf-to: lamer@aol.com

7:

8: Content-type: text/plain

9: Date: Wed, 15 Nov 2001 14:09:47 -0500

10: From: automated_beacon@somewhere.com

11: Subject: [Your Site] Thank You For Registering!

12: To: lamer@aol.com

13:

14: Thank you for registering with us! Keep this e-mail for future reference.

15:

16: etc. etc.

So, what we need to do is to capture the information from this directory and then set up rules to handle the different messages based on subject, from, to, date, anything that is in those standard headers.

The Code

After we first specify our default directory in a variable, we use the <CFDIRECTORY> tag to select the contents of the UNDELIVR directory and deposit them into a query: <CFDIRECTORY ACTION="LIST" DIRECTORY="#tmpUndeliverDefaultPath#" NAME="qryDir" SORT="Name" FILTER="*.cfmail">. This will allow us to loop through the contents of the directory and then run a particular action based on different criteria.

Next, we read the file using the <CFFILE> tag and extract the pertinent information we will need to determine how to handle each e-mail. Then, once we have the contents of the file, we can deposit the contents in an array by treating the file contents as a list that is delimited by the line feed character using the function Chr(10).

<CFFILE ACTION="READ" FILE="#tmpUndeliverDefaultPath##qryDir.Name#" VARIABLE="tmpTxt">

<CFSET tmpEmailContents = ListToArray(tmpTxt,Chr(10))>

So, now we have the contents of .cfmail file in a format that we can easily use. This is where the line numbers from earlier come into play. We can use the line numbers because they match up with the contents of the array. For example, the To address is on line six, so it is also in the sixth element in the array. We can set a variable that contains this text, but since it contains the whole line, including the prefix "x-cf-to:", we'll use the ReplaceNoCase() function to remove it so that we only have the contents of the element we need:

<CFSET tmpEmail = Trim(ReplaceNoCase(tmpEmailContents[6],"x-cf-to:",""))>

However, this only works for the first six lines of the .cfmail file. After that the location of the elements can vary from e-mail to e-mail, specifically, when the <CFMAILPARAM> tag is used. To find an element that varies in location, we simply loop through the elements that can vary (which start at element 8) until we find it. Below is an example of how we would retrieve the Subject element, whose location is not guaranteed:

<CFLOOP FROM="8" TO="#ArrayLen(tmpEmailContents)#" INDEX="i">

<CFIF Find("Subject:", tmpEmailContents[i])>

<CFSET tmpEmailLoc = i>

<CFBREAK>

</CFIF>

</CFLOOP>

<CFSET tmpSubject = Trim(ReplaceNoCase(tmpEmailContents[tmpEmailLoc],"Subject:",""))>

You can retrieve any elements in the header in this manner. If the element you need is in the first six lines, you can call it directly, otherwise, loop through until you find it.

Now we have the variables we need, it's just a matter of setting up the rules. If you are going to set up rules based on one variable, such as the Subject line, <CFSWITCH> would be a good way to go, otherwise, you'll need to use a combination of <CFIF>, <CFELSEIF> and <CFELSE> statements. Here are some example rules:

<CFIF tmpSubject IS "[Your Site] Thank You For Registering!">

<!--- Delete this person from the database. --->

<CFQUERY DATASOURCE="#DSN#" NAME="qryDelThisEmail">

DELETE

FROM tblMembers

WHERE txtEmail = '#tmpEmail#'

</CFQUERY>

<!--- Delete the message. --->

<CFFILE ACTION="DELETE" FILE="#tmpUndeliverDefaultPath##qryDir.Name#">

<CFELSEIF tmpEmail CONTAINS "@AtSomeDomainThatAlwaysHasProblems.com">

<CFIF ReFindNoCase("^([[:alnum:]][-a-zA-Z0-9_%\.]*)?[[:alnum:]]@[[:alnum:]][-a-zA-Z0-9%\>.]*\.[[:alpha:]]{2,}$", tmpEmail)>

<!--- If this e-mail is well-formed according to the regular expression above, try resending it. --->

<CFFILE ACTION="MOVE" SOURCE="#tmpUndeliverDefaultPath##qryDir.Name#" DESTINATION="#tmpSpoolDefaultPath##qryDir.Name#">

<CFELSE>

<!--- Otherwise, delete the e-mail. --->

<CFFILE ACTION="DELETE" FILE="#tmpUndeliverDefaultPath##qryDir.Name#">

</CFIF>

<CFELSE>

<!--- E-mail the Administrator --->

<CFMAIL TO="admin@somewhere.com" FROM="automated_beacon@somewhere.com" SUBJECT="[Your Site] Random Message">

Check out the undelivered message folder.

</CFMAIL>

</CFIF>

Or you could apply whatever other conditions you'd like, and have them perform any actions you'd like.

And That's It

If you wanted to automate this script, you could schedule it in ColdFusion or Windows Scheduler and have it run once a day or once a week, however often e-mail accumulates on your site.

Below is the final code. If you have any questions or improvements, leave your comments below.

<CFPARAM NAME="tmpUndeliverDefaultPath" DEFAULT="C:\CFUSION\MAIL\UNDELIVR\"><!--- Path to undelivered e-mail folder. --->

<CFPARAM NAME="tmpSpoolDefaultPath" DEFAULT="C:\CFUSION\MAIL\SPOOL\"><!--- Path to spool folder. --->

<!--- CFDirectory call to the aforementioned directory. --->

<CFDIRECTORY ACTION="LIST" DIRECTORY="#tmpUndeliverDefaultPath#" NAME="qryDir" SORT="Name" FILTER="*.cfmail">

<CFIF qryDir.RecordCount>

<CFOUTPUT QUERY="qryDir">

<CFFILE ACTION="READ" FILE="#tmpUndeliverDefaultPath##qryDir.Name#" VARIABLE="tmpTxt">

<CFSET tmpEmailContents = ListToArray(tmpTxt,Chr(10))>

<CFSET tmpEmail = Trim(ReplaceNoCase(tmpEmailContents[6],"x-cf-to:",""))>

<CFLOOP FROM="8" TO="#ArrayLen(tmpEmailContents)#" INDEX="i">

<CFIF Find("Subject:", tmpEmailContents[i])>

<CFSET tmpEmailLoc = i>

<CFBREAK>

</CFIF>

</CFLOOP>

<CFSET tmpSubject = Trim(ReplaceNoCase(tmpEmailContents[tmpEmailLoc],"Subject:",""))>

<CFIF tmpSubject IS "[Your Site] Thank You For Registering!">

<!--- Delete this person from the database. --->

<CFQUERY DATASOURCE="#DSN#" NAME="qryDelThisEmail">

DELETE

FROM tblMembers

WHERE txtEmail = '#tmpEmail#'

</CFQUERY>

<!--- Delete the message. --->

<CFFILE ACTION="DELETE" FILE="#tmpUndeliverDefaultPath##qryDir.Name#">

<CFELSEIF tmpEmail CONTAINS "@AtSomeDomainThatAlwaysHasProblems.com">

<CFIF ReFindNoCase("^([[:alnum:]][-a-zA-Z0-9_%\.]*)?[[:alnum:]]@[[:alnum:]][-a-zA-Z0-9%\>.]*\.[[:alpha:]]{2,}$", tmpEmail)>

<!--- If this e-mail is well-formed according to the regular expression above, try resending it. --->

<CFFILE ACTION="MOVE" SOURCE="#tmpUndeliverDefaultPath##qryDir.Name#" DESTINATION="#tmpSpoolDefaultPath##qryDir.Name#">

<CFELSE>

<!--- Otherwise, delete the e-mail. --->

<CFFILE ACTION="DELETE" FILE="#tmpUndeliverDefaultPath##qryDir.Name#">

</CFIF>

<CFELSE>

<!--- E-mail the Administrator --->

<CFMAIL TO="admin@somewhere.com" FROM="automated_beacon@somewhere.com" SUBJECT="[Your Site] Random Message">

Check out the undelivered message folder.

</CFMAIL>

</CFIF>

</CFOUTPUT>

</CFIF>

Big thanks to .jeff for all his help with this code.