Retrieving Email with ColdFusion


You have already seen how the <cfmail> tag can be used to send mail messages via your ColdFusion templates. You can also create ColdFusion templates that receive and process incoming mail messages. What your templates do with the messages is up to you. You might display each message to the user, or you might have ColdFusion periodically monitor the contents of a particular mailbox, responding to each incoming message in some way.

Introducing the <cfpop> Tag

To check or receive email messages with ColdFusion, you use the <cfpop> tag, providing the username and password for the email mailbox you want ColdFusion to look in. ColdFusion will connect to the appropriate mail server in the same way that your own email client program connects to retrieve your mail for you.

Table 27.5 lists the attributes supported by the <cfpop> tag.

Table 27.5. <cfpop> Tag Attributes

ATTRIBUTE

PURPOSE

action

GetHeaderOnly, GetAll, or Delete. Use GetHeaderOnly to quickly retrieve just the basic information (the subject, who it's from, and so on) about messages, without retrieving the messages themselves. Use GetAll to retrieve actual messages, including any attachments (which might take some time). Use Delete to delete a message from the mailbox.

server

Required. The POP server to which ColdFusion should connect. You can provide either a host name, such as pop.orangewhipstudios.com, or an IP address.

username

Required. The username for the POP mailbox ColdFusion should access. This is likely to be case sensitive, depending on the POP server.

password

Required. The password for the POP mailbox ColdFusion should access. This is likely to be case sensitive, depending on the POP server.

name

ColdFusion places information about incoming messages into a query object. You will loop through the records in the query to perform whatever processing you need for each message. Provide a name (such as GetMessages) for the query object here. This attribute is required if the action is GetHeaderOnly or GetAll.

maxrows

Optional. The maximum number of messages that should be retrieved. Because you don't know how many messages might be in the mailbox you are accessing, it's usually a good idea to provide maxrows unless you are providing messagenumber (later in this table).

startRow

Optional. The first message that should be retrieved. If, for instance, you already have processed the first 10 messages currently in the mailbox, you could specify startRow="11" to start at the 11th message.

messageNumber

Optional. If the action is GetHeaderOnly or GetAll, you can use this attribute to specify messages to retrieve from the POP server. If the action is Delete, this is the message or messages you want to delete from the mailbox. In either case, you can provide either a single message number or a comma-separated list of message numbers.

attachmentPath

Optional. If the action is GetAll, you can specify a directory on the server's drive in which ColdFusion should save any attachments. If you don't provide this attribute, the attachments won't be saved.

generateUniqueFilenames

Optional. This attribute should be provided only if you are using the attachmentPath attribute. If Yes, ColdFusion will ensure that two attachments that happen to have the same filename will get unique filenames when they are saved on the server's drive. If No (the default), each attachment is saved with its original filename, regardless of whether a file with the same name already exists in the attachmentPath directory.

port

Optional. If the POP server specified in server is listening for requests on a nonstandard port, specify the port number here. The default value is 110, which is the standard port used by most POP servers.

timeout

Optional. This attribute indicates how many seconds ColdFusion should wait for each response from the POP server. The default value is 60.


NOTE

The <cfpop> tag can only be used to check email that is sitting on a mail server that uses the Post Office Protocol (POP, or POP3). POP servers are by far the most popular type of mailbox server, largely because the POP protocol is very simple. Some mail servers use the newer Internet Mail Access Protocol (IMAP, or IMAP4). The <cfpop> tag can't be used to retrieve messages from IMAP mailboxes. Perhaps a future version of ColdFusion will include a <cfimap> tag; until then, some third-party solutions are available at the Developers Exchange Web site (http://www.macromedia.com/cfusion/exchange/index.cfm).


When the <cfpop> tag is used with action="GetHeaderOnly", it will return a query object that contains one row for each message in the specified mailbox. The columns of the query object are shown in Table 27.6.

Table 27.6. Columns Returned by <cfpop> When ACTION="GetHeaderOnly"

COLUMN

EXPLANATION

messageNumber

A number that represents the slot the current message is occupying in the mailbox on the POP server. The first message that arrives in a user's mailbox is message number 1. The next one to arrive is message number 2. When a message is deleted, any message behind the deleted message moves into the deleted message's slot. That is, if the first message is deleted, the second message becomes message number 1. In other words, the messageNumber isn't a unique identifier for the message. It's just a way to refer to the messages currently in the mailbox.

date

The date the message was originally sent. Unfortunately, this date value isn't returned as a native CFML Date object. You must use the ParseDateTime() function to turn the value into something you can use ColdFusion's date functions with (see Listing 26.6, later in this chapter, for an example).

subject

The subject line of the message.

from

The email address that the message is reported to be from. This address might or might not contain a friendly name for the sender, delimited by quotation marks and angle brackets (see the section "Using Friendly Email Addresses," earlier in this chapter). It's worth noting that there's no guarantee that the from address is a real email address that can actually receive replies.

to

The email address to which the message was sent. This address might or might not contain a friendly name for the sender, delimited by quotation marks and angle brackets (see the section "Using Friendly Email Addresses").

cc

The email address or addresses to which the message was CC'd, if any. You can use ColdFusion's list functions to get the individual email addresses. Each address might or might not contain a friendly name for the sender, delimited by quotation marks and angle brackets (see the section "Using Friendly Email Addresses").

replyTo

The address to use when replying to the message, if provided. If the message's sender didn't provide a Reply-To address, the column will contain an empty string, in which case it would be most appropriate for replies to go to the from address. This address might or might not contain a friendly name for the sender, delimited by quotation marks and angle brackets (see the section "Using Friendly Email Addresses").


If the <cfpop> tag is used with action="GetAll", the returned query object will contain all the columns from Table 27.6, plus the columns listed in Table 27.7.

Table 27.7. Additional Columns Returned by <cfpop> When action="GetAll"

COLUMN

EXPLANATION

body

The actual body of the message, as a simple string. This string usually contains just plain text, but if the message was sent as an HTML-formatted message, it contains HTML tags. You can check for the presence of a Content-Type header value of text/html to determine whether the message is HTML formatted (see Listing 27.8, later in this chapter, for an example).

header

The raw, unparsed header section of the message. This usually contains information about how the message was routed to the mail server, along with information about which program was used to send the message, the MIME content type of the message, and so on. You need to know about the header names defined by the SMTP protocol (see the section "Adding Custom Mail Headers," earlier in this chapter) to make use of the header.

attachment

If you provided an attachmentPath attribute to the <cfpop> tag, this column contains a list of the attachment filenames as they were named when originally attached to the message. The list of attachments is separated by tab characters. You can use ColdFusion's list functions to process the list, but you must specify Chr(9) (which is the tab character) as the delimiter for each list function, as in listLen(ATTACHMENTS, Chr(9)).

attachmentFiles

If you provided an attachmentPath attribute to the <cfpop> tag, this column contains a list of the attachment filenames as they were saved on the ColdFusion server (in the directory specified by attachmentPath). You can use the values in this list to delete, show, or move the files after the message has been retrieved. Like the attachments column, this list is separated by tab characters.


Retrieving the List of Messages

Most uses of the <cfpop> tag call for all three of the action values it supports. Whether you are using the tag to display messages to your users (such as a Web-based system for checking mail) or an automated agent that responds to incoming email messages on its own, the sequence of events probably involves these steps:

1.

Log in to the mail server with action="GetHeaderOnly" to get the list of messages currently in the specified mailbox. At this point, you can display or make decisions based on who the message is from, the date, or the subject line.

2.

Use action="GetAll" to retrieve the full text of individual messages.

3.

Use action="Delete" to delete messages.

Listing 27.6 is the first of three templates that demonstrate how to use <cfpop> by creating a Web-based system for users to check their mail. This template asks the user to log in by providing the information ColdFusion needs to access their email mailbox (username, password, and mail server). It then checks the user's mailbox for messages and displays the From address, date, and subject line for each. The user can click each message's subject to read the full message.

Listing 27.6. CheckMail.cfmThe Beginnings of a Simple POP Client
 <!---   Filename: CheckMail.cfm  Author: Nate Weiss (NMW)  Purpose: Creates a very simple POP client ---> <html> <head><title>Check Your Mail</title></head> <body> <!--- Simple CSS-based formatting styles ---> <style>  body {font-family:sans-serif;font-size:12px}  th {font-size:12px;background:navy;color:white}  td {font-size:12px;background:lightgrey;color:navy} </style> <h2>Check Your Mail</h2> <!--- If user is logging out, --->  <!--- or if user is submitting a different username/password ---> <cfif isDefined("URL.logout") or isDefined("FORM.popServer")>  <cfset structDelete(SESSION, "mail")> </cfif> <!--- If we don't have a username/password ---> <cfif not isDefined("SESSION.mail")>  <!--- Show "mail server login" form --->  <cfinclude template="CheckMailLogin.cfm"> </cfif> <!--- If we need to contact server for list of messages ---> <!--- (if just logged in, or if clicked "Refresh" link) ---> <cfif not isDefined("SESSION.mail.getMessages") or isDefined("URL.refresh")>  <!--- Flush page output buffer --->  <cfflush>  <!--- Contact POP Server and retieve messages --->  <cfpop action="GetHeaderOnly" name="SESSION.mail.getMessages"  server="#SESSION.mail.popServer#"  username="#SESSION.mail.username#" password="#SESSION.mail.password#"  maxrows="50"> </cfif>    <!--- If no messages were retrieved... ---> <cfif SESSION.mail.getMessages.recordCount eq 0>  <p>You have no mail messages at this time.<br>   <!--- If messages were retrieved... --->  <cfelse>  <!--- Display Messages in HTML Table Format --->   <table border="0" cellSpacing="2" cellSpacing="2" cols="3" width="550">  <!--- Column Headings for Table --->  <tr>  <th width="100">Date Sent</th>  <th width="200">From</th>  <th width="200">Subject</th>  </tr>  <!--- Display info about each message in a table row --->  <cfoutput query="SESSION.mail.getMessages">  <!--- Parse Date from the "date" mail header --->  <cfset msgDate = parseDateTime(date,"pop")>  <!--- Let user click on Subject to read full message --->  <cfset linkURL = "CheckMailMsg.cfm?MsgNum=#MessageNumber#">  <tr valign="baseline">  <!--- Show parsed Date and Time for message--->  <td>  <strong>#dateFormat(msgDate)#</strong><br>  #timeFormat(msgDate)# #ReplyTo#  </td>  <!--- Show "From" address, escaping brackets --->  <td>#htmlEditFormat(From)#</td>  <td><strong><a href="#linkURL#">#Subject#</a></strong></td>  </tr>   </cfoutput>  </table>   </cfif> <!--- "Refresh" link to get new list of messages ---> <strong><a href="CheckMail.cfm?Refresh=Yes">Refresh Message List</a></strong><br> <!--- "Log Out" link to discard SESSION.Mail info ---> <a href="CheckMail.cfm?Logout=Yes">Log Out</a><br> </body> </html> 

NOTE

Don't forget to copy the Application.cfc file from the CD. The code isn't described until listing 27.10.


This template maintains a structure in the SESSION scope called SESSION.mail. The SESSION.mail structure holds information about the current user's POP server, username, and password. It also holds a query object called getMessages, which is returned by the <cfpop> tag when the user's mailbox is first checked.

At the top of the template, a <cfif> test checks to see whether a URL parameter named logout has been provided. If so, the SESSION.mail structure is deleted from the server's memory, which effectively logs the user out. Later, you will see how this works. The same thing happens if a FORM parameter named popServer exists, which indicates that the user is trying to submit a different username and password from the login form. (I'll explain this in a moment.)

Next, a similar <cfif> tests checks to see whether the SESSION.mail structure exists. If not, the template concludes that the user hasn't logged in yet, so it displays a simple login form by including the CheckMailLogin.cfm template (see Listing 27.7). This is the same basic login-check technique explained in Chapter 21. In any case, all code after this <cfif> test is guaranteed to execute only if the user has logged in. The SESSION.mail structure will contain username, password, and popServer values, which can later be passed to all <cfpop> tags for the remainder of the session.

The next <cfif> test checks to see whether ColdFusion needs to access the user's mailbox to get a list of current messages. ColdFusion should do this whenever SESSION.mail.getMessages doesn't exist yet (which means that the user has just logged in), or if the page has been passed a refresh parameter in the URL (which means that the user has just clicked the Refresh Message List link, as shown in Figure 27.6). If so, the <cfpop> tag is called with action="GetHeaderOnly", which means that ColdFusion should get a list of messages from the mail server (which is usually pretty fast), rather than getting the actual text of each message (which can be quite slow, especially if some of the messages have attachments). Note that the <cfpop> tag is provided with the username, password, and POP server name that the user provided when they first logged in (now available in the SESSION.mail structure).

Figure 27.6. The <CFPOP> tag enables email messages to be retrieved via ColdFusion templates.


NOTE

The SESSION.mail.getMessages object returned by the tag contains columns called Date, Subject, From, To, CC, ReplyTo, and MessageNumber, as listed previously in Table 26.6. Because it's a query object, it also contains the automatic CurrentRow and RecordCount attributes returned by ordinary <cfquery> tags.


At this point, the template has retrieved the list of current messages from the user's mailbox, so all that's left to do is to display them to the user. The remainder of Listing 27.6 simply outputs the list of messages in a simple HTML table format, using an ordinary <cfoutput> block that loops over the SESSION.mail.getMessages query object. Within this loop, the code can refer to the Date column of the query object to access a message's date or to the Subject column to access the message's subject line. The first time through the loop, these variables refer to the first message retrieved from the user's mailbox. The second time, the variables refer to the second message, and so on.

Just inside the <cfoutput> block, a ColdFusion date variable called msgDate is created using the parseDateTime() function with the optional POP attribute. This is necessary because the Date column returned by <cfpop> doesn't contain native CFML date value, as you might expect. Instead, it contains the date in the special date format required by the mail-sending protocol (SMTP). The parseDateTime() function is needed to parse this special date string into a proper CFML Date value you can provide to ColdFusion's date functions (such as the dateFormat() and dateAdd() functions).

NOTE

Unfortunately, the format used for the date portion of mail messages varies somewhat. The parseDateTime() function doesn't properly parse the date string that some incoming messages have. If the function encounters a date that it can't parse correctly, an error message results. Some custom tag solutions to this problem are available at the ColdFusion Developers Exchange Web site (http://www.macromedia.com/cfusion/exchange/index.cfm). Search for POP and Date.


Inside the <cfoutput> block, the basic information about the message is output as a table row. The date of the message is shown using the dateFormat() and timeFormat() functions. The Subject line of the message is presented as a link to the CheckMailMsg.cfm template (see Listing 27.8), passing the MessageNumber for the current message in the URL. Because the MessageNumber identifies the message in a particular slot in the mailbox, the user can click the subject to view the whole message.

At the bottom of the template, the user is provided with Refresh Message List and Log Out links, which simply reload the listing with either refresh=Yes or logout=Yes in the URL.

NOTE

Because email addresses can contain angle brackets (see the section "Using Friendly Email Addresses," earlier in this chapter), you should always use the htmlEditFormat() function when displaying an email address returned by <cfpop> in a Web page. Otherwise, the browser will think the angle brackets are meant to indicate an HTML tag, which means that the email address won't show up visibly on the page (although it will be part of the page's HTML if you view source). Here, htmlEditFormat() is used on the From column, but you should use it whenever you output the To, CC, or ReplyTo columns as well.


Listing 27.7 is a template that presents a login form to the user when they first visit CheckMail.cfm. It's included via the <cfinclude> tag in Listing 27.6 whenever the SESSION.mail structure doesn't exist (which means that the user has either logged out or has not logged in yet).

Listing 27.7. CheckMailLogin.cfmA Simple Login Form, Which Gets Included by CheckMail.cfm
 <!---   Filename: CheckMailLogin.cfm  Author: Nate Weiss (NMW)  Purpose: Provides a login form for the simple POP client ---> <!--- If user is submitting username/password form ---> <cfif isDefined("FORM.popServer")>  <!--- Retain username, password, server in SESSION --->  <cfset SESSION.mail = structNew()>  <cfset SESSION.mail.popServer = FORM.popServer>  <cfset SESSION.mail.username = FORM.username>  <cfset SESSION.mail.password = FORM.password>  <!--- Remember server and username for next time --->   <cfset CLIENT.mailServer = FORM.popServer>  <cfset CLIENT.mailUsername = FORM.username> <cfelse>   <!--- Use server/username from last time, if available --->  <cfparam name="CLIENT.mailServer" type="string" default="">  <cfparam name="CLIENT.mailUsername" type="string" default="">    <!--- Simple form for user to provide mailbox info --->  <cfform action="#CGI.script_name#" method="post">  <p>To access your mail, please provide the   server, username and password.<br>    <!--- FORM field: POPServer --->  <p>Mail Server:<br>  <cfinput type="text" name="popServer"   value="#CLIENT.mailServer#" required="Yes"   message="Please provide your mail server.">   (example: pop.yourcompany.com)<br>    <!--- FORM field: Username --->   Mailbox Username:<br>  <cfinput type="text" name="username"  value="#CLIENT.mailUsername#" required="Yes"  message="Please provide your username.">   (yourname@yourcompany.com)<br>    <!--- FORM field: Password --->   Mailbox Password:<BR>  <cfinput type="password" name="password"  required="yes"   message="Please provide your password."><br>    <cfinput type="submit" name="submit" value="Check Mail"><br>  </cfform>    </body></html>  <cfabort> </cfif> 

When the user first visits CheckMail.cfm, Listing 27.7 gets included. At first, the FORM.popServer variable won't exist, so the <cfelse> part of the code executes, which presents the login form to the user. When the form is submitted, it posts the user's entries to the CheckMail.cfm template, which in turn calls this template again. This time, FORM.popServer exists, so the first part of the <cfif> block executes. The SESSION.mail structure is created, and the popServer, username, and password values are copied from the user's form input into the structure so that they can be referred to during the rest of the session, or until the user logs out.

If you accidentally enter an incorrect username or password while testing this listing, you will get a rather ugly error message. You can intercept the error so that the user is kindly asked to try again, without it seeming like anything has really gone so wrong. A revised version of this listing that does just that is included in Chapter 32, "Error Handling."

NOTE

As a convenience to the user, Listing 27.7 stores the popServer and username values (which the user provides in the login form) as variables in the CLIENT scope. These values are passed to the value attributes of the corresponding form fields the next time the user needs to log in. This way, the user only needs to enter their password on repeat visits.


Receiving and Deleting Messages

Listing 27.8 is the CheckMailMsg.cfm template the user will be directed to whenever they click the subject line in the list of messages (refer to Figure 27.6). This template requires that a URL parameter called MsgNum be passed to it, which indicates the messagenumber of the message the user clicked. In addition, the template can be passed a Delete parameter, which indicates that the user wants to delete the specified message.

Listing 27.8. CheckMailMsg.cfmRetrieving the Full Text of an Individual Message
 <!---   Filename: CheckMailMsg1.cfm  Author: Nate Weiss (NMW)  Purpose: Allows the user to view a message on their POP server ---> <html> <head><title>Mail Message</title></head> <body> <!--- Simple CSS-based formatting styles ---> <style>  body {font-family:sans-serif;font-size:12px}  th {font-size:12px;background:navy;color:white}  td {font-size:12px;background:lightgrey;color:navy} </style> <h2>Mail Message</h2> <!--- A message number must be passed in the URL ---> <cfparam name="URL.msgNum" type="numeric"> <cfparam name="URL.delete" type="boolean" default="No"> <!--- If we don't have a username/password ---> <!--- send user to main CheckMail.cfm page ---> <cfif not isDefined("SESSION.mail.getMessages")>  <cflocation url="CheckMail.cfm"> </cfif> <!--- If the user is trying to delete the message ---> <cfif URL.delete>  <!--- Contact POP Server and delete the message --->  <cfpop action="Delete" messagenumber="#URL.msgNum#"  server="#SESSION.mail.popServer#"  username="#SESSION.mail.username#"  password="#SESSION.mail.password#">  <!--- Send user back to main "Check Mail" page --->   <cflocation url="CheckMail.cfm?refresh=Yes"> <!--- If not deleting, retrieve and show the message ---> <cfelse>  <!--- Contact POP Server and retrieve the message --->  <cfpop action="GetAll" name="GetMsg"  messagenumber="#URL.msgNum#"  server="#SESSION.mail.popServer#"  username="#SESSION.mail.username#"  password="#SESSION.mail.password#">    <cfset msgDate = parseDateTime(getMsg.Date, "pop")>     <!--- If message was not retrieved from POP server --->   <cfif getMsg.recordCount neq 1>  <cfthrow message="Message could not be retrieved."  detail="Perhaps the message has already been deleted.">  </cfif>     <!--- We will provide a link to Delete message --->  <cfset deleteURL = "#CGI.script_name#?msgNum=#MsgNum#&delete=Yes">    <!--- Display message in a simple table format --->   <table border="0" cellSpacing="0" cellPadding="3">  <cfoutput>  <tr>  <th bgcolor="wheat" align="left" nowrap>  Message #URL.msgNum# of #SESSION.mail.getMessages.recordCount#  </th>  <td align="right" bgcolor="beige">  <!--- Provide "Back" button, if appropriate --->   <cfif URL.msgNum gt 1>  <a href="CheckMailMsg.cfm?msgNum=#decrementValue(URL.msgNum)#">  <img src="/books/2/448/1/html/2/../images/browseback.gif"   width="40" height="16" alt="Back" border="0"></a>  </cfif>  <!--- Provide "Next" button, if appropriate --->  <cfif URL.msgNum lt SESSION.mail.getMessages.recordCount>  <a href="CheckMailMsg.cfm?msgNum=#incrementValue(URL.msgNum)#">  <img src="/books/2/448/1/html/2/../images/browsenext.gif"   width="40" height="16" alt="Next" border="0"></a>  </cfif>  </td>  </tr>  <tr>  <th align="right">From:</th>  <td>#htmlEditFormat(getMsg.From)#</td>  </tr>  <cfif getMsg.CC neq "">  <tr>  <th align="right">CC:</th>  <td>#htmlEditFormat(getMsg.CC)#</td>  </tr>  </cfif>  <tr>  <th align="right">Date:</th>  <td>#dateFormat(msgDate)# #timeFormat(msgDate)#</td>  </tr>  <tr>  <th align="right">Subject:</th>  <td>#getMsg.Subject#</td>  </tr>  <tr>  <td bgcolor="beige" colspan="2">  <strong>Message:</strong><br>    <cfif getMsg.Header contains "Content-Type: text/html">  #getMsg.Body#   <cfelse>  #htmlCodeFormat(getMsg.Body)#  </cfif>  </td>  </tr>  </cfoutput>  </table>   <cfoutput>   <!--- Provide link back to list of messages --->  <strong><a href="CheckMail.cfm">Back To Message List</a></strong><br>  <!--- Provide link to Delete message --->  <a href="#deleteURL#">Delete Message</a><br>  <!--- "Log Out" link to discard SESSION.Mail info --->  <a href="CheckMail.cfm?Logout=Yes">Log Out</a><br>  </cfoutput> </cfif> </body> </html> 

NOTE

Be sure to save the previous listing as CheckMailMsg.cfm, not CheckMailMsg1.cfm.


As a sanity check, the user is first sent back to the CheckMail.cfm template (refer to Listing 27.6) if the SESSION.mail.getMessages query doesn't exist. This would happen if the user's session had timed out or if the user had somehow navigated to the page without logging in first. In any case, sending them back to CheckMail.cfm causes the login form to be displayed.

Next, a <cfif> test is used to see whether delete=Yes was passed in the URL. If so, the message is deleted using the action="Delete" attribute of the <cfpop> tag, specifying the passed URL.msgNum as the messagenumber to delete. The user is then sent back to CheckMail.cfm with refresh=Yes in the URL, which causes CheckMail.cfm to re-contact the mail server and repopulate the SESSION.mail.getMessages query with the revised list of messages (which should no longer include the deleted message).

If the user isn't deleting the message, the template simply retrieves and displays it in a simple HTML table format. To do so, <cfpop> is called again, this time with action="GetAll" and the messagenumber of the desired message. Then the columns returned by <cfpop> can be displayed, much as they were in Listing 27.6. Because the action was "GetAll", this template could use the BODY and HEADER columns listed previously in Table 27.7. The end result is that the user has a convenient way to view, scroll through, and delete messages, as shown in Figure 27.7.

Figure 27.7. With <CFPOP>, retrieving and displaying the messages in a user's POP mailbox is easy.


At the bottom of the template, users are provided with the links to log out or return to the list of messages. They are also provided with a link to delete the current message, which simply reloads the current page with delete=Yes in the URL, causing the Delete logic mentioned previously to execute.

Receiving Attachments

As noted previously in Table 27.5, the <cfpop> tag includes an attachmentPath attribute that, when provided, tells ColdFusion to save any attachments to a message to a folder on the server's drive. Your template can then process the attachments in whatever way is appropriate: move the files to a certain location, parse through them, display them to the user, or whatever your application needs.

Retrieving the Attachments

Listing 27.9 is a revised version of the CheckMailMsg.cfm template from Listing 27.8. This version enables the user to download and view any attachments that might be attached to each mail message. The most important change in this version is the addition of the attachmentPath attribute, which specifies that any attachments should be placed in a subfolder named Attach (within the folder that the template itself is in).

Listing 27.9. CheckMailMsg2.cfmAllowing the User to Access Attachments
 <!---   Filename: CheckMailMsg2.cfm  Author: Nate Weiss (NMW)  Purpose: Allows the user to view a message on their POP server ---> <html> <head><title>Mail Message</title></head> <body> <!--- Simple CSS-based formatting styles ---> <style>  body {font-family:sans-serif;font-size:12px}  th {font-size:12px;background:navy;color:white}  td {font-size:12px;background:lightgrey;color:navy} </style> <h2>Mail Message</h2> <!--- A message number must be passed in the URL ---> <cfparam name="URL.msgNum" type="numeric"> <cfparam name="URL.delete" type="boolean" default="no"> <!--- Store attachments in "Attach" subfolder ---> <cfset attachDir = expandPath("Attach")> <!--- Set a variable to hold the Tab character ---> <cfset TAB = chr(9)>   <!--- Create the folder if it doesn't already exist ---> <cfif not directoryExists(attachDir)>  <cfdirectory action="create" directory="#attachDir#"> </cfif> <!--- If we don't have a username/password ---> <!--- send user to main CheckMail.cfm page ---> <cfif not isDefined("SESSION.mail.getMessages")>  <cflocation url="CheckMail.cfm"> </cfif> <!--- If the user is trying to delete the message ---> <cfif url.delete>  <!--- Contact POP Server and delete the message --->  <cfpop action="Delete"  messagenumber="#URL.msgNum#"  server="#SESSION.mail.popServer#"  username="#SESSION.mail.username#"  password="#SESSION.mail.password#">  <!--- Send user back to main "Check Mail" page --->   <cflocation url="CheckMail.cfm?refresh=Yes"> <!--- If not deleting, retrieve and show the message ---> <cfelse>  <!--- Contact POP Server and retrieve the message --->  <cfpop action="GetAll" name="GetMsg"  messagenumber="#URL.msgNum#"  server="#SESSION.mail.popServer#"  username="#SESSION.mail.username#"  password="#SESSION.mail.password#"  attachmentPath="#attachDir#"  generateUniqueFilenames="Yes">    <!--- Parse message's date string to CF Date value --->   <cfset msgDate = parseDateTime(getMsg.Date, "POP")>     <!--- If message was not retrieved from POP server --->   <cfif getMsg.recordCount neq 1>  <cfthrow   message="Message could not be retrieved."  detail="Perhaps the message has already been deleted.">  </cfif>     <!--- We will provide a link to Delete message --->  <cfset deleteURL = "#CGI.script_name#?msgNum=#msgNum#&delete=Yes">    <!--- Display message in a simple table format --->   <table border="0" cellSpacing="0" cellPadding="3">  <cfoutput>  <tr>  <th bgcolor="wheat" align="left" nowrap>  Message #URL.msgNum# of #SESSION.mail.getMessages.recordCount#  </th>  <td align="right" bgcolor="beige">  <!--- Provide "Back" button, if appropriate --->   <cfif URL.msgNum gt 1>  <a href="CheckMailMsg.cfm?msgNum=#decrementValue(URL.msgNum)#">  <img src="/books/2/448/1/html/2/../images/browseback.gif"   width="40" height="16" alt="Back" border="0"></a>  </cfif>  <!--- Provide "Next" button, if appropriate --->  <cfif URL.msgNum lt SESSION.mail.getMessages.recordCount>  <a href="CheckMailMsg.cfm?msgNum=#incrementValue(URL.msgNum)#">  <img src="/books/2/448/1/html/2/../images/browsenext.gif"   width="40" height="16" alt="Next" border="0"></a>  </cfif>  </td>  </tr>  <tr>  <th align="right">From:</th>  <td>#htmlEditFormat(getMsg.From)#</td>  </tr>  <cfif getMsg.CC neq "">  <tr>  <th align="right">CC:</th>  <td>#htmlEditFormat(getMsg.CC)#</td>  </tr>  </cfif>  <tr>  <th align="right">Date:</th>  <td>#dateFormat(msgDate)# #timeFormat(msgDate)#</td>  </tr>  <tr>  <th align="right">Subject:</th>  <td>#getMsg.Subject#</td>  </tr>  <tr>  <td bgcolor="beige" colspan="2">  <strong>Message:</strong><br>    <cfif getMsg.Header contains "Content-Type: text/html">  #getMsg.Body#   <cfelse>  #htmlCodeFormat(getMsg.Body)#  </cfif>  </td>  </tr>  <!--- If this message has any attachments --->  <cfset numAttachments = listLen(getMsg.Attachments, TAB)>  <cfif numattachments gt 0>  <tr>  <th align="right">Attachments:</th>  <td>  <!--- For each attachment, provide a link --->  <cfloop from="1" to="#numAttachments#" index="i">  <!--- Original filename, as it was attached to message --->  <cfset thisFileOrig = listGetAt(getMsg.Attachments, i, TAB)>  <!--- Full path to file, as it was saved on this server --->  <cfset thisFilePath = listGetAt(getMsg.attachmentFiles, i, TAB)>  <!--- Relative URL to file, so user can click to get it --->  <cfset thisFileURL = "Attach/#getFileFromPath(thisFilePath)#">  <!--- Actual link --->  <a href="#thisFileURL#">#thisFileOrig#</a><br>  </cfloop>  </td>  </tr>  </cfif>  </cfoutput>  </table>   <cfoutput>   <!--- Provide link back to list of messages --->  <strong><a href="CheckMail.cfm">Back To Message List</a></strong><br>  <!--- Provide link to Delete message --->  <a href="#deleteURL#">Delete Message</a><br>  <!--- "Log Out" link to discard SESSION.Mail info --->  <a href="CheckMail.cfm?logout=Yes">Log Out</a><br>  </cfoutput> </cfif> </body> </html> 

NOTE

Be sure to save the previous listing as CheckMailMsg.cfm, not CheckMailMsg2.cfm.


The first change is the addition of a variable called attachDir, which is the complete path to the directory on the server that will hold attachment files. Additionally, a variable called TAB is created to hold a single tab character (which is character number 9 in the standard character set). This way, the rest of the code can refer to TAB instead of chr(9), which improves code readability.

NOTE

This code uses the variable name TABE6in all caps instead of tab to indicate the notion that the variable holds a constant value. A constant value is simply a value that will never change (that is, the tab character will always be represented by ASCII code 9). Developers often write constants in all capital letters to make them stand out. You don't have to do this, but you might find it helpful as you write your ColdFusion templates.


NOTE

This code uses the expandPath() function to set attachDir to the subfolder named Attach within the folder that the template itself is in. See Appendix C, "ColdFusion Function Reference," for details about expandPath().


Next, a directoryExists() test checks to see whether the attachDir directory exists yet. If not, the directory is created via the <cfdirectory> tag. See Chapter 34 for details about creating directories on the server. After the directory is known to exist, it's safe to provide the value of attachDir to the attachmentPath attribute of the <cfpop> tag.

NOTE

This code also sets generateUniqueFilenames to Yes so there is no danger of two attachment files with the same name (from different messages, say) being overwritten with one another. It's generally recommended that you do this to prevent the risk of two <cfpop> requests interfering with one another.


Now, near the bottom of the template, the attachments and attachmentFiles columns of the getMsg query object are examined to present any attachments to the user. As noted previously in Table 27.7, these two columns contain tab-separated lists of the message's file attachments (if any). Unlike most ColdFusion lists, these lists are separated with tab characters, so any of ColdFusion's list functions must specify the tab character as the delimiter.

For instance, the numAttachments variable is set to the number of file attachments using a simple call to the listLen() function. If at least one attachment exists, a simple <cfloop> block iterates through the list of attachments. Each time through the loop, the thisFileOrig variable holds the original filename of the attachment (as the sender attached it), the thisFilePath variable holds the unique filename used to save the file in attachDir, and the thisFileURL variable holds the appropriate relative URL for the file on the server. It's then quite easy to provide a simple link the user can click to view or save the file (as shown in Figure 27.8).

Figure 27.8. The <CFPOP> tag can retrieve files attached to messages in a mailbox.


Deleting Attachments After Use

One problem with Listing 27.9 is the fact that the attachment files that get placed into attachDir are never deleted. Over time, the directory would fill up with every attachment for every message that was ever displayed by the template. It would be nice to delete the files when the user was finished looking at the message, but because of the stateless nature of the Web, you don't really know when that is. You could delete each user's attachment files when they log out, but the user could close their browser without logging out. Luckily, the latest version of ColdFusion allows you to run code automatically when a session ends. This is done via the onSessionEnd() method of the Application.cfc file.

Listing 26.10 lists the Application.cfc file for the application.

Listing 26.10. Application.cfcDeleting Attachment Files Previously Saved by <CFPOP>
 <!---   Filename: Application.cfc  Created by: Raymond Camden (ray@camdenfamily.com)  Please Note Executes for every page request ---> <cfcomponent output="false">   <!--- Name the application. --->   <cfset this.name="OrangeWhipSite">   <!--- Turn on session management. --->   <cfset this.sessionManagement=true>   <cfset this.clientManagement=true>   <cffunction name="onSessionEnd" output="false" returnType="void">     <!--- Look for attachments to delete --->     <cfset var attachDir = expandPath("Attach")>     <cfset var getFiles = "">     <cfset var thisFile = "">     <!--- Get a list of all files in the directory --->     <cfdirectory directory="#attachDir#" name="getFiles">     <!--- For each file in the directory --->      <cfloop query="getFiles">       <!--- If it's a file (rather than a directory) --->       <cfif getFiles.type neq "Dir">         <!--- Get full filename of this file --->         <cfset thisFile = expandPath("Attach\#getFiles.Name#")>         <!--- Go ahead and delete the file --->         <cffile action="delete" file="#thisFile#">       </cfif>     </cfloop>   </cffunction> </cfcomponent> 

Most of this file simply handles turning on CLIENT and SESSION management. The onSessionEnd() method is what we are concerned with. This method will fire when the user's session expires. It uses <cfdirectory> to get all the files in the attachment directory. It then loops over and deletes each file.

Another Approach

CheckMailMsg3.cfm (which is on this book's CD-ROM) is yet another version of CheckMailMsg.cfm that uses a different approach to provide the user with access to the attachments. When the user first views the message, the attachment filenames are displayed on the page, but the actual attachments are immediately deleted from disk. If the user clicks an attachment, the page is accessed again, this time passing the name of the desired attachment in the URL. The template code reexecutes, this time returning the requested file via the <cfcontent> tag.

For details about the <cfcontent> and <cfheader> tags used in this template, see Chapter 33, "Generating Non-HTML Content."


The end result is that the user can access the files without the files ever needing to be stored on the server's drive. There is a significant downside, however, which is that the message is being re-retrieved from the server whenever the user clicks an attachment. If the attachments are many or large, this could mean quite a bit of extra processing time for ColdFusion. In general, the previous approach (Listing 27.9 coupled with Listing 27.10) is likely to serve you better in the long run.

An interesting side effect of this approach is that the attachment files don't need to reside in the Web server's document root because they will be accessed only via <cfcontent>. Therefore, the attachDir folder is set to the value returned by the getTempDirectory() function, which is a reasonable place to store files that need to exist for only a short time.

See Appendix C for information about getTempDirectory().


Creating Automated POP Agents

You can create automated agents that watch for new messages in a particular mailbox and respond to the messages in some kind of intelligent way. First, you create an agent template, which is just an ordinary ColdFusion template that checks a mailbox and performs whatever type of automatic processing is necessary. This template shouldn't contain any forms or links, because it won't be viewed by any of your users. Then, using the ColdFusion scheduler, you schedule the template to be visited automatically every ten minutes, or whatever interval you feel is appropriate.

Creating the Agent Template

Listing 27.11 creates a simple version of a typical agent template: an unsubscribe agent, which responds to user requests to be removed from mailing lists. If you look at the SendBulkMail.cfm template (refer to Listing 27.4), you will notice that all messages sent by the template include instructions for users who want to be removed from Orange Whip Studios' mailing list.

The instructions tell the user to send a reply to the email with the word Remove in the subject line. Therefore, the main job of this template is to check the mailbox to which the replies will be sent (which is mailings@orangewhipstudios.com in this example). The template then checks each message's subject line. If it includes the word Remove, and the sender's email address is found in the Contacts table, the user is removed from the mailing list by setting the user's MailingList field to 0 in the database. The next time the SendBulkMail.cfm is used to send a bulk message, the user will be excluded from the mailing.

Listing 27.11. ListUnsubscriber.cfmAutomatically Unsubscribing Users from a Mailing List
 <!---   Filename: ListUnsubscriber.cfm  Author: Nate Weiss (NMW)  Purpose: A simple automated POP agent for unsubscribing from mailing lists ---> <!--- Mailbox info for "mailings@orangewhipstudios.com" ---> <cfset popServer = "pop.orangewhipstudios.com"> <cfset username = "mailings"> <cfset password = "ThreeOrangeWhips"> <!--- We will delete all messages in this list ---> <cfset msgDeleteList = ""> <html> <head><title>List Unsubscriber Agent</title></head> <body> <h2>List Unsubscriber Agent</h2>   <p>Checking the mailings@orangewhipstudios.com mailbox for new messages...<br>  This may take a minute, depending on traffic and the number of messages.<br> <!--- Flush output buffer so the above messages ---> <!--- are shown while <CFPOP> is doing its work ---> <cfflush> <!--- Contact POP Server and retrieve messages ---> <cfpop action="GetHeaderOnly" name="getMessages"  server="#popServer#" username="#username#" password="#password#"  maxrows="20">   <!--- Short status message --->  <cfoutput>  <p><strong>#getMessages.recordCount# messages to process.</strong><br> </cfoutput>    <!--- For each message currently in the mailbox... ---> <cfloop query="getMessages">  <!--- Short status message --->  <cfoutput>  <p><strong>Message from:</strong> #htmlEditFormat(getMessages.From)#<br>  </cfoutput>  <!--- If the subject line contains the word "Remove" --->  <cfif getMessages.Subject does not contain "Remove">    <!--- Short status message --->    Message does not contain "Remove".<br>  <cfelse>     <!--- Short status message --->    Message contains "Remove".<br>    <!--- Which "word" in From address contains @ sign? --->    <cfset addrPos = listFind(getMessages.From, "@", "<> ")>    <!--- Assuming one of the "words" contains @ sign, --->    <cfif addrPos eq 0>      <!--- Short status message --->      Address not found in From line.<BR>    <cfelse>        <!--- Email address is that word in From address --->      <cfset fromAddress = trim(listGetAt(getMessages.From, addrPos, "<> "))>        <!--- Who in mailing list has this email address? --->      <cfquery name="getContact" datasource="ows" maxrows="1">      SELECT ContactID, FirstName, LastName       FROM Contacts      WHERE Email = '#fromAddress#'      AND MailingList = 1      </cfquery>      <!--- Assuming someone has this address... --->       <cfif getContact.recordCount eq 0>       <!--- Short status message --->       <cfoutput>Recipient #fromAddress# not on list.<br></cfoutput>      <cfelse>        <!--- Short status message --->        <cfoutput>Removing #fromAddress# from list.<br></cfoutput>          <!--- Update the database to take them off list --->        <cfquery datasource="ows">        UPDATE Contacts SET        MailingList = 0        WHERE ContactID = #getContact.ContactID#         </cfquery>        <!--- Short status message --->        Sending confirmation message via email.<br>          <!--- Mail user a confirmation note --->        <cfmail        to="""#getContact.FirstName# #getContact.LastName#"" <#fromAddress#>"        from="""Orange Whip Studios"" <mailings@orangewhipstudios.com>"        subject="Mailing List Request"        >You have been removed from our mailing list.</cfmail>        </cfif>     </cfif>   </cfif>    <!--- Add this message to the list of ones to delete. --->  <!--- If you wanted to only delete some messages, you --->  <!--- would put some kind of <CFIF> test around this. --->  <cfset msgDeleteList = listAppend(msgDeleteList, getMessages.MessageNumber)> </cfloop> <!--- If there are messages to delete ---> <cfif msgDeleteList neq "">   <!--- Short status message --->   <p>Deleting messages...   <!--- Flush output buffer so the above messages --->   <!--- are shown while <CFPOP> is doing its work --->   <cfflush>     <!--- Contact POP Server and delete messages --->   <cfpop action="Delete" server="#popServer#"   username="#username#"   password="#password#"   messageNumber="#msgDeleteList#">     Done.<br>  </cfif> </body> </html> 

The code in Listing 27.11 is fairly simple. First, the <cfpop> tag is used to retrieve the list of messages currently in the appropriate mailbox. Please note that you will need to change the popServer, username, and password settings, Because the template needs to look only at the Subject line of each message, this template only needs to perform this GetHeaderOnly action. It never needs to retrieve the entirety of each message via action="GetAll".

Then, for each message, a series of tests are performed to determine whether the message is indeed a removal request from someone who is actually on the mailing list. First, the template checks to see if the Subject line contains the word Remove. If so, it now must extract the sender's email address from the string in the message's From line (which might contain a friendly name or just an email address). To do so, the template uses ListFind() to determine which word in the From lineif anycontains an @ sign, where each word is separated by angle brackets or spaces. If such a word is found, that word is assumed to be the user's email address and is stored in the fromAddress variable via the listGetAt() function.

Next, the query named getContact is run to determine whether a user with the email address in question does indeed existand hasn't already been removed from the mailing list. If the query returns a row, the email is coming from a legitimate email address, so it represents a valid removal request.

NOTE

The getContact query uses maxrows="1" just in case two users exist in the database with the email address in question. If so, only one is removed from the mailing list.


The next <cfquery> updates the sender's record in the Contacts table, setting the MailingList column to 0, effectively removing them from the mailing list. Finally, the sender is sent a confirmation note via a <cfmail> tag, so they know their remove request has been received and processed.

The <cfloop> then moves on to the next message in the mailbox, until all messages have been processed. With each iteration, the current MessageNumber is appended to a simple ColdFusion list called msgDeleteList. After all messages have been processed, they are deleted from the mailbox using the second <cfpop> tag at the bottom of the template. As the template executes, messages are output for debugging purposes, so you can see what the template is doing if you visit using a browser (see Figure 27.9).

Figure 27.9. Automated POP agents can scan a mailbox for messages and act appropriately.


NOTE

If you use the <cfflush> tag before each <cfpop> tag, as this template does, the messages output by the page is displayed in real time as the template executes. See Chapter 24, "Improving the User Experience," for details about <cfflush>.


Scheduling the Agent Template

Once you have your agent template working properly, you should schedule for automatic, periodic execution using the ColdFusion Administrator or the <cfschedule> tag.

See Chapter 36 for details.


Other Uses for POP Agents

This example simply created a POP-based agent template that responds to unsubscribe requests. It could be expanded to serve as a full-fledged list server, responding to both subscribe and unsubscribe requests. It could even be in charge of forwarding incoming messages back out to members of the mailing list.

That said, ColdFusion isn't designed to be a round-the-clock, high-throughput, mail-generating engine. If you will be sending out tens of thousands of email messages every hour, you should probably think about a different solution. You wouldn't want your ColdFusion server to be so busy tending to its mail delivery duties that it wasn't capable of responding to Web page requests in a timely fashion.

Other POP-based agents could be used to create auto-responder mailboxes that respond to incoming messages by sending back standard messages, perhaps with files attached. You could also create a different type of agent that examines incoming help messages for certain words and sends back messages that should solve the user's problem.



Macromedia Coldfusion MX 7 Web Application Construction Kit
Macromedia Coldfusion MX 7 Web Application Construction Kit
ISBN: 321223675
EAN: N/A
Year: 2006
Pages: 282

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net