POP3 Email Client with Attachments
By Robert Jones
This tutorial is quite simple if you are familiar with many
of the basic practices of ColdFusion. It outlines exactly how to create a webbased
email client for all of your clients currently hosted by your company. It is
basically an expansion on Pablo Varando's
Email client, which is all well and good for those of us that are web-savvy
and dont care about alert boxes, but in my experiance clients that aren't very
familiar with the internet tend to freak out and think something is wrong with
their computers when they see and alert box, which tends to be a hassle when
you provide customer support and they are calling you every five seconds because
of a simple harmless alert.
Therefore I set out to provide an extreamly user friendly email sending and
reciveing client. The files associated with thie tutorial are as follows:
application.cfm - A simple session management application
index.cfm - this is our login page, you could realistically
name this one login.cfm, but remember we are going for user friendlyness
inbox.cfm - This one should be obvious, delete.cfm and
send.cfm are set as cfincludes here
details.cfm - viewing messages and providing a way to
reply/forward/delete the messages
compose.cfm - create new messages, reply to messages,
and also forward messages
delete.cfm - delete messages from the POP3 server
send.cfm - send messages
upload.cfm - this is used for attaching messages to outgoing
emails.
LinkFinder.cfm - a simple custom tag that parses urls
in received messages and makes them active http://www.cfcomet.com/utilities/CF_LinkFinder_v1-1.zip
File: application.cfm
this file isn't to hard to figure out, it's simply to manage active sessions
you can set the timespan to be how ever long you feel neccisary
<cfapplication name="name"
sessionmanagement="yes"
sessiontimeout="#CreateTimeSpan(0,0,10,0)#">
^Top
File: index.cfm
This is our login screen, it gives the user the opportunity to remember their
user ID so that they don't have to enter it everytime they come to check their
email, because people like that sort of thing ;-P
Note: I only use this system for those of my clients that are hosted on MY system
and thus all have the same POP3 server, but it is easily adaptable for those
of you who wish to use it for anyone.
<!--- If the user has logged out, we clear
the session variables --->
<cfif IsDefined('logout')>
<cfscript>
StructClear(Session);
</cfscript>
<!--- if the cookie was set and the user wants
to sign in as someone else, we clear the cookie --->
<cfelseif IsDefined('newuser')>
<cfcookie name="username"
expires="now">
<cfscript>
StructClear(Cookie);
</cfscript>
</cfif>
<!--- If the user is still active we simply
redirect them right to their Inbox --->
<cfif IsDefined('session.username')
and IsDefined('session.userpass')>
<cflocation url="inbox.cfm"
addtoken="yes">
</cfif>
<!--- Our login form --->
<form method="post"
action="inbox.cfm?login=try">
<table border="0"
width="80%"
cellspacing="0"
cellpadding="0"
align="center">
<!--- If we have a session timeout or an invalid
username/password, display an error message here --->
<cfif IsDefined('error')>
<tr>
<td colspan="2"
align="center">
<span
style="color: red">*
Invalid Username or Password</span>
</td>
</tr>
</cfif>
<tr>
<td align="right"
width="50%">
<font
face="Arial" size="2">Username: </font>
</td>
<td align="left"
width="50%">
<!--- If the user has chosen to remember their
username we insert it as a hidden feild and display it in bold letters --->
<cfif
IsDefined('cookie.username')>
<cfoutput>
<input
type="hidden" name="userNAME"
value="#cookie.username#">
<font
face="Arial" size="2"><b>#cookie.username#</b></font>
</cfoutput>
<!--- If they haven't set thier cookies, we
display a text input --->
<cfelse>
<input
type="text" name="userNAME"
size="30"
>
</cfif>
</td>
</tr>
<!--- We do not want to remember the users password
in the cookie, so this is just a plain ole' password field --->
<tr>
<td align="right"
width="50%">
<font
face="Arial" size="2">Password: </font>
</td>
<td align="left"
width="50%">
<input type="password"
name="userPASS"
size="30">
</td>
</tr>
<tr>
<td colspan="2"
align="center">
<!--- If the cookies haven't been set yet, we
ask the user if the WANT them set via a checkbox --->
<cfif
NOT IsDefined('cookie.username')>
<input
type="checkbox" name="remember"
value="indeed"><font
face="Arial" size="2">
Remember ID on this Computer?</font>
<!---
If the cookies ARE set we ask them if the are the user that is set by the cookie,
if not they can log out, if so we pass the value of "remember" along
in a hidden field
--->
<cfelse>
<cfoutput>
<input
type="hidden" name="remember"
value="indeed">
<font
face="Arial" size="2">Not
#cookie.username#? <a href="index.cfm?newuser=true">Login
as a different user</a></font>
</cfoutput>
</cfif>
</td>
</tr>
<tr>
<td colspan="2"
align="center">
<input type="submit"
value="Log In">
</td>
</tr>
</table>
</form>
^Top
File: inbox.cfm
This is the meat and potatoes of our application, without this page, we wouldn't
be able to Recieve, send OR delete Messages, so its a tad bit important.
The formatting and layout of this page can be EASILY changed to fit you needs,
but you probably already knew that right?
<!---
Some if/else statements to start us off here
First we find out if the user filled out the login form,
or if they were simply redirected because their session
was still valid.
--->
<cfif IsDefined('login')>
<!---
If the user did use the login form,
we check to make sure the user entered data,
if so, we set the form variables up as session variables for easy use
--->
<cfif IsDefined('form.userNAME')
and #form.userPASS#
is not "">
<cfset session.username=#form.userNAME#>
<cfset session.userpass=#form.userPASS#>
<!---
If they want us to remember there login info We'll set a cookie
otherwise we will set a cookie with the same name to expire now
that way there will deffinently be no cookies set on the users machine
--->
<cfif IsDefined('form.remember')>
<cfcookie name="username"
value="#form.userNAME#"
expires="never">
<cfelse>
<cfcookie name="username"
expires="now">
</cfif>
<!---
If the user didn't use the login form OR they didn't enter valid information
Redirect them to the index screen with error set to true
--->
<cfelse>
<cflocation url="index.cfm?error=true"
addtoken="yes">
</cfif>
</cfif>
<!---
Here is where the sending and deleting includes come in
We check to see if an action is defined at all,
then we find out what that function is and include the appropriete file for
that action
--->
<cfif isDefined('action')>
<cfif action is
"send">
<cfinclude template="send.cfm">
<cfelseif action is
"delete">
<cfinclude template="delete.cfm">
</cfif>
</cfif>
<!---
This is where we retrieve the messages from the POP3 server
We've surrounded the CFPOP tag with a CFTRY statement
This is another way to check to be sure that the user entered valid data
if there are any errors thrown back at us we redirect the user to the login
screen
this is what they are used to anyway right?
--->
<cftry>
<cfpop action="getheaderonly"
name="qGetMessages"
server="mail.yoursever.com"
timeout="90"
username="#session.username#"
password="#session.userpass#">
<cfcatch type="Any">
<cflocation url="index.cfm?error=true">
<cfabort>
</cfcatch>
</cftry>
<!---
This is just a header,
I like to have an image here at the top,
you can of course put whatever you want here
--->
<table width="100%" border="0">
<tr>
<td align="left">
<img src="images/logo.gif">
</td>
<td align="right">
<font size="2"
face="Arial">
<cfoutput>
#session.username#<br>
<a href="index.cfm?logout=true">Log
Out</a>
</font>
</cfoutput>
</td>
</tr>
</table>
<table width="100%"
border="0"
cellpadding="3"
cellspacing="0">
<tr bgcolor="#000000">
<td width="44%">
<font size="2"
face="Arial"
color="white">
<b>Inbox</b>
</font>
</td>
<td width="54%"
align="center">
<font size="2"
face="Arial"
color="red"><b>
<!---
This is our conformation cell,
if the user sent or deleted something,
this is where we tell them
Its just one of those user friendly kinda things
--->
<cfif IsDefined('sent')>
--MESSAGE SENT--
<cfelseif
IsDefined('delete')>
--MESSAGE DELETED--
</cfif>
</b></font>
</td>
<td width="1%"
colspan="2"
align="right">
<a href="inbox.cfm">Refresh</a>
</td>
<td width="1%"
align="right">
<font color="#FFFFFF"> || </font><a
href="compose.cfm">Compose</a>
</td>
</tr>
</table>
<!---
This is our column headers,
They are displayed regaurdless of weather or not there are messages
so there is really nothing all that special about them
--->
<table width="100%"
border="0"
cellpadding="2"
cellspacing="1">
<tr bgcolor="#efefef">
<td width="14%"
colspan="2">
<font size="2"
face="Arial">
<b>DATE</b>
</font>
</td>
<td width="20%">
<font size="2"
face="Arial">
<b>FROM</b>
</font>
</td>
<td width="40%">
<font size="2"
face="Arial">
<b>SUBJECT</b>
</font>
</td>
<td width="26%"
colspan="2"
align="center">
<font size="2"
face="Arial">
<b>ACTIONS</b>
</font>
</td>
</tr>
<!---
We wrap the whole thing in a form
that way we can delete multiple messages all at once
the checkboxes have a value that is equal to the message number
when/if the form is submitted it only deletes those messages that have been
checked
how convenient right?
--->
<form action="inbox.cfm?action=delete"
method="post">
<tr>
<td colspan="2">
<cfif qGetMessages.RecordCount>
<cfoutput
query="qGetMessages">
<!---
Alternating row colors
ooooooooo, ahhhhhh
--->
<tr
bgcolor="###iif(currentrow MOD 2,DE('D7D7D7'),DE('EBEBEB'))#">
<td
width="1%">
<input
type="checkbox" name="msgnumber"
value="#qGetMessages.messagenumber#">
</td>
<td
width="13%">
<font
size="2" face="Arial">
<!---
I used a regular expression to format the POP date
I know there are other ways to do it, this was just the simplest for me
--->
#reReplace(left(qGetMessages.date,
21), "^(.* ).*", "\1")#
</font>
</td>
<td
width="20%">
<font
size="2" face="Arial">
#qGetMessages.from#
</font>
</td>
<td
width="40%">
<font
size="2" face="Arial">
#qGetMessages.subject#
</font>
</td>
<!---
These are our actions here,
we can delete the message directly,
or we can go ahead and view and see what it says,
then decide what to do from there
--->
<td
width="8%" align="center">
<a
href="details.cfm?msgnumber=#qGetMessages.messagenumber#">View</a>
</td>
<td
width="8%" align="center">
<a
href="inbox.cfm?action=delete&msgnumber=#qGetMessages.messagenumber#">Delete</a>
</td>
</tr>
</cfoutput>
<tr>
<td colspan="6">
<input type="submit"
value="Delete Checked"
name="delete">
</td>
</tr>
</table>
</form>
<!---
If there are no messages in the inbox
we display this simple litle message telling them so
--->
<cfelse>
<tr bgcolor="#D7D7D7">
<td width="100%"
colspan="6"
align="center">
<font size="2"
face="Arial">
There are no messages in your
inbox.
</font>
</td>
</tr>
</table>
</cfif>
^Top
File: details.cfm
If you want to be able to read your messages than you will most deffinently
need this here lil file, this file is also responcable for retreiving all of
the attachments included with the email, downloading them to a directory specific
to the username, and then linking out to the file, I have not worked out how
to periodically delete the files from the server, but I'm sure it cant be all
that hard, if you really need me to please just contact
me and I will put something together.
<!---
This is just a formatting tool
it replaces the line breaks and tabs with their HTML components
--->
<cfscript>
function pformat(str)
{
str = replace(str,chr(13)&chr(10),chr(10),"ALL");
str = replace(str,chr(13),chr(10),"ALL");
str = replace(str,chr(9)," ","ALL");
return replace(str,chr(10),"<br>","ALL");
}
</cfscript>
<!---
We have to define a few variables for the attachments
we will create a directory named with the user name
if it doesn't already exsist we will go ahead and create it
--->
<cfset Directory =
'#session.username#'>
<cfset AttachDir =
ExpandPath("#Directory#")>
<cfset TAB = Chr(9)>
<cfif NOT
DirectoryExists(AttachDir)>
<cfdirectory action="create"
directory="#AttachDir#">
</cfif>
<!---
Get the full message from the server
I dont create unique file names because we don't put all the files into the
same directory
thus we can leave the file names as they were originally sent
--->
<cfpop action="GETALL"
messagenumber="#url.msgnumber#"
name="qGetMessageDetails"
username="#session.username#"
password="#session.userpass#"
server="mail.yourserver.com"
attachmentpath="#AttachDir#"
generateuniquefilenames="no">
<table width="100%"
border="0"
align="right">
<tr>
<td align="left">
<img src="images/mail.gif">
</td>
<td align="right">
<font size="2"
face="Arial">
<cfoutput>
#session.username#<br>
</cfoutput>
<a
href="index.cfm?logout=true">Log
Out</a>
</font>
</td>
</tr>
</table>
<table width="100%"
border="0"
cellpadding="3"
cellspacing="0">
<tr bgcolor="#000000">
<td width="30%">
<font size="2"
face="Arial"
color="white">
<b>View
Message</b>
</font>
</td>
<td width="40%"
align="center">
</td>
<td width="30%"
colspan="2"
align="right">
<a href="inbox.cfm">Back
to Inbox</a>
</td>
</tr>
</table>
<br>
<cfoutput query="qGetMessageDetails">
<table width="70%"
border="0" align="center">
<tr>
<td colspan="2">
<table width="1%"
border="0">
<tr>
<td
valign="top">
<a
href="compose.cfm?action=reply&msgnumber=#msgnumber#">Reply</a>
</td>
<td
valign="top">
<a
href="compose.cfm?action=forward&msgnumber=#msgnumber#">Forward</a>
</td>
<td
valign="top">
<a
href="inbox.cfm?action=delete&msgnumber=#msgnumber#">Delete</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<font
size="2" face="Arial">
<b>From:</b>
#from#<BR>
<b>Date:</b>
#date#<BR>
<b>Subject:</b>
#subject#<BR>
</font>
</td>
</tr>
<tr>
<td colspan="2">
<hr
noshade>
<font
size="2" face="Arial">
<b>Message:</b><BR>
<!---
This is a custom tag that I got from http://www.cfcomet.com/utilities/CF_LinkFinder_v1-1.zip
It simply finds and makes active all the links in the text,
nifty little tag I daresay
we also use the pformat() function that we defined earlier in the page
--->
<CF_LinkFinder
DATA="#qGetMessageDetails.body#"
NAME="newbody">
#pformat(newbody)#
<BR>
<BR>
<!---
look for attachments,
if they exsist, download them to the directory we created
then display them as links
if they don't exsist, do nothing
--->
<cfset NumAttachments =
ListLen(qGetMessageDetails.Attachments, TAB)>
<cfif NumAttachments GT
1>
<hr noshade color="##000000">
<b>Attachments:</b><br>
<cfloop from="1"
to="#NumAttachments#"
index="i">
<cfset ThisFileOrig
= ListGetAt(qGetMessageDetails.Attachments,
i, TAB)>
<cfset ThisFilePath
= ListGetAt(qGetMessageDetails.Attachments,
i, TAB)>
<cfset ThisFileURL =
"#Directory#/#GetFileFromPath(ThisFilePath)#">
<a href="#ThisFileURL#">#ThisFilePath#</a><br>
</cfloop>
<BR>
</cfif>
<table
width="1%" border="0">
<tr>
<td
valign="top">
<a
href="compose.cfm?action=reply&msgnumber=#msgnumber#">Reply</a>
</td>
<td
valign="top">
<a
href="compose.cfm?action=forward&msgnumber=#msgnumber#">Forward</a>
</td>
<td
valign="top">
<a
href="inbox.cfm?action=delete&msgnumber=#msgnumber#">Delete</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</cfoutput>
^Top
File: compose.cfm
We will be compose new messages and reply/forwarded messages in this file,
its quite simple, just one big ole form with a few CFIF statements.
<!---
Only if an action is defined do we grab the information off the server
if not we just skip this step altogether
--->
<cfif IsDefined('action')>
<cfpop action="GETALL"
messagenumber="#url.msgnumber#"
name="qGetMessageDetails"
username="#session.username#"
password="#session.userpass#"
server="mail.yourserver.com">
</cfif>
<table width="100%"
border="0">
<tr>
<td align="left">
<img src="images/mail.gif">
</td>
<td align="right">
<font size="2"
face="Arial">
<cfoutput>
#session.username#<br>
</cfoutput>
<a href="index.cfm?logout=true">Log
Out</a>
</font>
</td>
</tr>
</table>
<table width="100%"
border="0" cellpadding="3"
cellspacing="0">
<tr bgcolor="#000000">
<td width="30%">
<!---
If an action is defined we display that action
otherwise we just display "Compose Message"
Once again this is just one of those user friendly features
--->
<font size="2"
face="Arial"
color="white"><b>
<cfif IsDefined('action')>
<cfif
action is "reply">
Reply to Message
<cfelseif
action is "forward">
Forward Message
</cfif>
<cfelse>
Compose Mail
</cfif>
</b></font>
</td>
<td width="40%"
align="center">
</td>
<td width="30%"
align="right">
<a href="inbox.cfm">Back
to Inbox</a>
</td>
</tr>
</table>
<!---
we set the ENCTYPE to multipart/form-data
This is REQUIRED if you plan to send attachments along with the email
if you do not wish to add the attachment feature this setting is not required
but it will not effect anything to leave it there, so I sugesst just leaving it
as is
--->
<form action="inbox.cfm"
method="post" enctype="multipart/form-data">
<input type="hidden" name="action"
value="send">
<table width="70%"
border="0" align="center">
<tr>
<td width="15%"
height="24" align="right">
<font size="2"
face="Arial">
To:
</font>
</td>
<td>
<input type='text'
name='to_email' size="70"
<!---
when replying we fill in the field with the senders address
--->
<cfif
IsDefined('action')>
<cfif
action is "reply">
<cfoutput
query="qGetMessageDetails">
value='#qGetMessageDetails.From#'
</cfoutput>
</cfif>
</cfif>
>
</td>
</tr>
<tr>
<td align="right"
width="15%">
<font size="2"
face="Arial">
CC:
</font>
</td>
<td align="left"
valign="middle">
<input type='text' name='CC_email'
size="30">
<font size="2"
face="Arial">
BCC:
</font>
<input type="text"
name="bcc_email" size="31">
</td>
</tr>
<tr>
<td align="right"
width="15%">
<font size="2"
face="Arial">
Subject:
</font>
</td>
<td>
<input type="text"
name="subject" size="70"
<!---
If an action has been defined we prefill the field
if not we have just a blank field ready to be filled in
--->
<cfif
IsDefined('action')>
<cfif
action is "reply">
<cfoutput
query="qGetMessageDetails">
value='Re:
#qGetMessageDetails.Subject#'
</cfoutput>
<cfelseif
action is "forward">
<cfoutput query="qGetMessageDetails">
value='Fwd:
#qGetMessageDetails.Subject#'
</cfoutput>
</cfif>
</cfif>
>
</td>
</tr>
<tr>
<td align="right"
width="15%">
<font size="2"
face="Arial">
Attach Files:
</font>
</td>
<td>
<input type="file"
name="Attachment" size="58">
</td>
</tr>
<tr>
<td align="center"
colspan="2">
<hr noshade color="#000000">
<font size="2"
face="Arial">
Message:
</font>
</td>
</tr>
<tr>
<td align="center"
colspan="2">
<!---
If an action is defined we prefill the textarea with the original message
we use the CFPROCESSINGDIRECTIVE tag so that the CF software doesnt squish it
all together
thus leaving the original message in the form that it was sent in, with line breaks
and everything
--->
<cfif IsDefined('action')>
<cfoutput
query="qGetMessageDetails">
<cfprocessingdirective
suppresswhitespace="no">
<textarea name="reply_message"
rows="15" cols="80">
-----Original Message-----
From: #qGetMessageDetails.from#
Sent: #qGetMessageDetails.date#
To: #qGetMessageDetails.to#
Subject: #qGetMessageDetails.subject#
#qGetMessageDetails.Body#</textarea>
</cfprocessingdirective>
</cfoutput>
<cfelse>
<textarea
name="reply_message" rows="15"
cols="80"></textarea>
</cfif>
</td>
</tr>
<tr>
<td align="center"
colspan="2">
<input type="submit"
name="Process" value="Send
Mail">
</td>
</tr>
</table>
</form>
^Top
File: delete.cfm
This is a simple little included tag, it simply deletes all the messages you want
deleted.
<!--- delete the message specified --->
<cfpop action="DELETE"
server="mail.yourserver.com"
username="#session.username#"
password="#session.userpass#"
messagenumber="#msgnumber#">
<cfset delete = "true">
^Top
File: send.cfm
This is where we send emails, also the upload.cfm file is called as an include
if the attachment field is not blank
<cfif #form.Attachment#
is not "">
<cfinclude template="upload.cfm">
</cfif>
<cfmail to="#to_email#" from="#session.username#"
subject="#subject#" cc="#CC_email#"
bcc="#bcc_email#">
#reply_message#
<cfif #form.Attachment#
is not "">
<cfmailparam file="#File.ServerDirectory#\#File.ServerFile#">
</cfif>
</cfmail>
<cfset sent = "true">
^Top
File: upload.cfm
Simple little included tag in the send.cfm file it simply uploads the attached
file to a directory specified.
<cfset Directory =
'#session.username#_uploads'>
<cfset UploadDir =
ExpandPath("#Directory#")>
<cfset TAB = Chr(9)>
<cfif NOT DirectoryExists(UploadDir)>
<cfdirectory action="create"
directory="#UploadDir#">
</cfif>
<cffile action="upload"
destination="#UploadDir#\" nameconflict="overwrite"
filefield="Attachment">
^Top
File: LinkFinder.cfm
This is a custom tag that was not written by me, please go to http://www.cfcomet.com/utilities/CF_LinkFinder_v1-1.zip
to find it. This isn't relly REQUIRED for the application, but if you choose
not to use it please remove the caller in details.cfm
^Top