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> TagTo 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.
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.
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.
Retrieving the List of MessagesMost 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:
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 MessagesListing 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 AttachmentsAs 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 AttachmentsListing 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 UseOne 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 ApproachCheckMailMsg3.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 AgentsYou 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 TemplateListing 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 TemplateOnce 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 AgentsThis 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. |