(Developer Shed Network) Server Side - PHP - Building A PHP-Based Mail Client (Part 1)
(Developer Shed Network) Server Side - PHP - Building A PHP-Based Mail Client (Part 1)
Table of Contents
Simply M@gical..................................................................................................................................................1 Requiring Immediate Attention .........................................................................................................................2 Start Me Up.........................................................................................................................................................4 Fully Functional................................................................................................................................................7 Opening Up..........................................................................................................................................................8 Treating Messages As Objects.........................................................................................................................15 Calling The Exterminator................................................................................................................................19 Back To Square One.........................................................................................................................................21
Simply M@gical
It's almost hard to believe that, up until a few years ago, putting pen to paper was still the most common method of corresponding with longlost relatives or distant business partners. Today, email is allpervasive it has a user base ranging from doting grandmothers to overenthusiastic dotcommers and is, by far, the fastest, most efficient way to communicate. Arthur C. Clarke once said that any sufficiently advanced technology was indistinguishable from magic; with millions of messages crisscrossing the globe at any given time, email has some pretty potent magic of its own. As a developer, email, and the systems designed to process it, have always fascinated me. Ever since I got my first email account, I've always found there to be something magical about the process by which a text message is encoded and bounced around the world from one mail server to another until it reaches its recipient, thence to be decoded back into its original form and displayed. And so, when I was offered the opportunity to work on a Webbased email client a few weeks back, I jumped at it; here, at last, was my chance to learn a little bit more about what actually happens after you hit the "Send" button... As it turned out, building a mail client wasn't anywhere near as hard as I thought it would be...and with the help of powerful opensource tools like PHP, the process was simplified considerably. Over the course of this article, I'm going to demonstrate how, by building a PHPbased mail client suitable for reading and writing email in any Web browser. The goal here is twofold: to introduce novice and intermediate programmers to the process of designing and implementing a Webbased application, with special reference to PHP's mail functions, and to offer road warriors, network administrators, email buffs and other interested folk a functional (and fairly goodlooking) email solution for use on their corporate intranet or Web site. Lofty goals, you scoff? Well, let's see...
Simply M@gical
Building A PHPBased Mail Client (part 1) workstation, or upload them for attachment to a new message. This is a fairly standard feature set, and you'll find that almost every mail client allows you to perform these actions. Note that the list above is somewhat abridged the actual requirements document was a bit more detailed, and included some additional items that will not be discussed here but it still has enough material to give you a fairly good idea of what I'll be covering in this case study. Putting down software requirements is a good starting point for any project, both from the implementation point of view and for other, related activities. Once the requirements are written down and approved by the customer, the developer can begin thinking about how to design and code the application, the interface designer can begin work on the application's user interface, and the QA team can begin building test cases to verify the final release of the code.
Start Me Up
With the requirements clearly defined, it's time to actually start writing some code. Since I'm a big fan of PHP, I plan to use that as my weapon of choice during this exercise. My natural inclination towards PHP is further influenced by the fact that PHP comes with a fullfeatured set of commands for working with IMAP and POP3 mailboxes something I'm going to be needing over the course of this project. This is a good time for you to download the source code, so that you can refer to it throughout this article (you will need a Web server capable of running PHP 4.0.6, with its IMAP extension enabled). mail.zip First up, the user login process, and the scripts which verify the user's password and grant access to the mail server. Here's the initial login form, "index.php":
<form name="login" method="POST" action="<? echo $PHP_SELF; ?>"> <table border="0" cellspacing="5" cellpadding="5" align="center" valign="middle"> <tr> <td align="right"><font face="Verdana" size="1">Email address</font></td> <td align="left"><input type=text name=email size=30></td> </tr> <tr> <td align="right"><font face="Verdana" size="1">Password</font></td> <td align="left"><input type=password name=pass size=10></td> </tr> <td colspan="2" align="middle"><input name="submit" type="submit" value="Read Mail"></td> </tr> </table> </form>
Extremely simple, this two fields, one for the user's email address, in the form <[email protected]> and one for his password. Here's what it looks like:
Start Me Up
Now, this script is actually split into two parts: the first part displays the login form above, while the second part processes the data entered into it. An "if" loop, keyed against the presence of the $submit variable, is used to decide which part of the script to execute. Here's what happens once the form is submitted:
<? // index.php display login form if (!$submit) { // form not yet submitted // display login box } else { ?> // form submitted include("functions.php"); if (!$email || !$pass || !validate_email($email)) { header("Location: error.php?ec=1"); exit; } // separate email address into username and hostname // by splitting on @ symbol $arr = explode('@', $email); $user = trim(stripslashes($arr[0])); $host = trim(stripslashes($arr[1])); $pass = trim(stripslashes($pass)); Start Me Up 5
Building A PHPBased Mail Client (part 1) // store the details in session variables session_start(); session_register("SESSION"); session_register("SESSION_USER_NAME"); session_register("SESSION_USER_PASS"); session_register("SESSION_MAIL_HOST"); // assign values to the session variables $SESSION_USER_NAME = $user; $SESSION_USER_PASS = $pass; $SESSION_MAIL_HOST = $host; // redirect user to the list page header("Location: list.php"); } ?>
The first order of business is to verify that all the information required to connect to the mail server has been entered by the user; this information includes a valid email address and password. Assuming that both are present, the explode() function is used to split the email address into user and host components. Next, a PHP session is initiated, and the username, password and host name are registered as session variables with the session_register() command; these values can then be used by other scripts within the application. Finally, the browser is redirected to another script, "list.php", which uses the information supplied to attempt a POP3 connection and retrieve a list of messages in the user's mailbox. This redirection is accomplished by sending an HTTP header containing the new URL to the browser via PHP's very powerful header() command. It's important to note that calls to header() and session_start() must take place before *any* output is sent to the browser. Even something as minor as whitespace or a carriage return outside the PHP tags can cause these calls to barf all over your script.
Start Me Up
Fully Functional
Before moving on, a quick word about the "functions.php" file include()d in the script you just saw. "functions.php" is a separate file containing useful function definitions. Every time I write a function that might come in useful elsewhere in the application, I move it into "functions.php" and include that file in my script. An example of this is the validate_email() function used in the script above here's what it looks like:
<? // check if email address is valid function validate_email($val) { if($val != "") { $pattern = "/^([azAZ09])+([\.azAZ09_])*@([azAZ09_])+(\.[azAZ09_]+)+/"; if(preg_match($pattern, $val)) { return true; } else { return false; } } else { return false; } } ?>
Again, this is fairly simple I'm using PHP's pattern matching capabilities to verify that the email address supplied conforms to the specified pattern. The function returns true or false depending on whether or not the match was successful.
Fully Functional
Opening Up
With the session instantiated, the next step is to retrieve and display a list of messages from the user's mailbox on the mail server. This is accomplished via "list.php", a PHP script which opens a connection to the POP3 server, obtains a list of message headers and displays them in a neat table. Before getting into the details of "list.php", I want to take a minute to explain a little something about the user interface. If you look at the sample screenshots in this article, you'll see that every page generated through this application has some common elements: the logo at the top left corner, the copyright note at the top right corner, and a dividing bar containing the page title below both. Since these elements will remain constant through the application, I've placed the corresponding HTML code in a separate header file, and simply include()d them on each page. Take a look at "header.php":
<table width="100%" border="0" cellspacing="0" cellpadding="5"> <tr> <td><img src="images/logo.jpg" width=67 height=55 alt="" border="0"></td> <td valign="bottom" align="right"><font size="2" face="Verdana">Everything here is © <a href="http://www.melonfire.com/">Melonfire</a>, 2001.<br>All rights reserved.</font></td> </tr> <tr> <td bgcolor="#C70D11" align="left"><font size="1" color="white" face="Verdana"><b><? echo $title; ?></b></font></td> <td bgcolor="#C70D11" align="right"><? if(session_is_registered("SESSION")) { ?><font size="1" color="white" face="Verdana"><b><a style="color: white" href="logout.php">Log Out</a></b></font><? } ?> </td> </tr> </table>
Again, by separating common interface elements into separate files, I've made it easier to customize the look of the application; simply alter these files, and the changes will be reflected on all the pages. Note that the page title needs to be specified as a variable prior to including this header file in a script you'll see examples of this over the next few pages. Note also the link to log out, which appears only after the user has logged in (I've used the session_is_registered() function to test for the presence of a valid PHP session). Okay, back to "list.php". Here's the script:
Opening Up
<?php // list.php display message list // includes include("functions.php"); // session check session_start(); if (!session_is_registered("SESSION")) { header("Location: error.php?ec=2"); exit; } // open mailbox $inbox = @imap_open ("{". $SESSION_MAIL_HOST . "/pop3:110}", $SESSION_USER_NAME, $SESSION_USER_PASS) or header("Location: error.php?ec=3"); // get number of messages $total = imap_num_msg($inbox); ?> <html> <head> </head> <body bgcolor="White"> <? // page header $title = "Message Listing ($total total)"; include("header.php"); ?> <table width="100%" border="0" cellspacing="3" cellpadding="5"> <! command buttons snipped > </table>
Opening Up
Building A PHPBased Mail Client (part 1) <form action="delete.php" method="post"> <! message info columns > <tr> <td width="5%"><font size="1"> </font></td> <td width="5%"><font size="1"> </font></td> <td width="15%"><font face="Verdana" size="1"><b>Date</b></font></td> <td width="20%"><font face="Verdana" size="1"><b>From</b></font></td> <td width="45%"><font face="Verdana" size="1"><b>Subject</b></font></td> <td width="10%"><font face="Verdana" size="1"><b>Size</b></font></td> </tr>
<?php // iterate through messages for($x=$total; $x>0; $x) { // get header and structure $headers = imap_header($inbox, $x); $structure = imap_fetchstructure($inbox, $x); ?> <tr bgcolor="<?php echo $bgcolor; ?>"> <td align="right" valign="top"> <input type="Checkbox" name="dmsg[]" value="<? echo $x; ?>"> </td> <td valign="top"> <? // attachment handling code goes here ?> </td> <td valign="top"> <font face="Verdana" size="1"><? echo substr($headers>Date, 0, 22); ?></font> </td> <td valign="top"> <font face="Verdana" size="1"><? echo htmlspecialchars($headers>fromaddress); ?></font> </td> <td valign="top"> <font face="Verdana" size="1"> <a href="view.php?id=<? echo $x; ?>"> <? // correction for empty subject
Opening Up
10
Building A PHPBased Mail Client (part 1) if ($headers>Subject == "") { echo "No subject"; } else { echo $headers>Subject; } ?> </a> </font> </td> <td valign="top"> <font face="Verdana" size="1"> <? // display message size echo ceil(($structure>bytes/1024)), " KB"; ?> </font> </td> </tr> <? } // clean up imap_close($inbox); ?> </form> </table> <? } else { echo "<font face=Verdana size=1>You have no mail at this time</font>"; } ?> </body> </html>
This probably looks complicated, but it isn't really. Let's take it from the top: 1. The first few lines of the script are pretty standard I've included the common function definitions, and tested for the presence of a valid session (and, by implication, the presence of a mail username, password and host).
<? // includes
Opening Up
11
Building A PHPBased Mail Client (part 1) // session check session_start(); if (!session_is_registered("SESSION")) { header("Location: error.php?ec=2"); exit; } ?>
In the event that this test fails, the browser is immediately redirected to the generic error handler, "error.php", with an error code identifying the problem. You'll see this code in almost every script that follows; it's a standard validation routine I plan to perform throughout the application. 2. Assuming that the session check is passed, the next step is to open a connection to the POP3 server. PHP offers the imap_open() function for this purpose; it accepts three parameters: the POP3 server name, the POP3 user name and the corresponding password (you can also use the imap_open() command to open a connection to an IMAP or NNTP server look at http://www.php.net/manual/en/ref.imap.php for examples).
<? // open mailbox $inbox = @imap_open ("{". $SESSION_MAIL_HOST . "/pop3:110}", $SESSION_USER_NAME, $SESSION_USER_PASS) or header("Location: error.php?ec=3"); ?>
If you're familiar with the POP3 protocol, this is equivalent to sending USER and PASS commands to the server. If the connection is successful, this function returns a handle representing the mailbox, required for all subsequent operations. In the event that a connection cannot be opened say, the password is wrong or the mail server is not active the browser is again redirected to "error.php" with an error code indicating the problem. 3. If a connection is successfully initiated, the imap_num_msg() function, in concert with the handle returned by imap_open(), is used to obtain the total number of messages in the mailbox; this number is then displayed in the page title.
<? // get number of messages $total = imap_num_msg($inbox); // page header $title = "Message Listing ($total total)";
Opening Up
12
Incidentally, don't be fooled by the prefix on all these function names; as stated previously, though every function starts with "imap_", PHP's IMAP extension can also be used with the POP3 and NNTP protocols. 4. Assuming that there are messages in the mailbox, an HTML table is generated to hold the message headers. In this case, I've decided to display the message date, subject, sender and size, together with a checkbox for message selection and an attachment icon if an attachment exists.
<? if ($total > 0) { ?> <table width="100%" border="0" cellspacing="0" cellpadding="5"> <form action="delete.php" method="post"> <! table rows and columns go here > </form> </table> <? } else { echo "<font face=Verdana size=1>You have no mail at this time</font>"; } ?>
Within the table, a "for" loop iterates as many times as there are messages, retrieving the headers and structure of each message with the imap_header() and imap_fetchstructure() functions respectively. Throughout this loop, the variable $x references the current message number. Note that I'm iterating through the message list in reverse order so that the more recent messages are displayed first.
<? // iterate through messages for($x=$total; $x>0; $x) { // get header and structure $headers = imap_header($inbox, $x); $structure = imap_fetchstructure($inbox, $x); // table rows here
Opening Up
13
If you're familiar with the POP3 protocol, this is equivalent to sending a series of RETR commands to the server.
Opening Up
14
$obj>fromaddress
$obj>Subject
would reference the message subject. The imap_header() function returns an object with the following properties, each corresponding to a different attribute of the mail message:
$obj>remail; $obj>date, $obj>Date, $obj>subject, $obj>Subject, $obj>in_reply_to, $obj>message_id, $obj>newsgroups, $obj>references $obj>toaddress $obj>fromaddress $obj>ccaddress $obj>bccaddress $obj>reply_toaddress $obj>senderaddress $obj>udate
For a complete list, take a look at the PHP manual page for this function at http://www.php.net/manual/en/function.imapheader.php Here's the code to print the message date and sender:
15
Building A PHPBased Mail Client (part 1) <font face="Verdana" size="1"><? echo substr($headers>Date, 0, 22); ?></font> </td> <td valign="top"> <font face="Verdana" size="1"><? echo htmlspecialchars($headers>fromaddress); ?></font> </td> <! snip >
I also need to link each message to a script, "view.php", which displays the complete message body. I've decided to do this by attaching a hyperlink to the subject of every message in the message list and passing it the message number via the URL GET method.
<td valign="top"> <font face="Verdana" size="1"> <a href="view.php?id=<? echo $x; ?>"> <? // correction for empty subject if ($headers>Subject == "") { echo "No subject"; } else { echo $headers>Subject; } ?> </a> </font> </td>
If you look at the list above, you'll see that the other two elements of my proposed message listing the message size and the attachment status are not available through imap_header(). So what do I do? 6. The answer, as it turns out, lies in another function: imap_fetchstructure(). Using a mailbox handle and message number as arguments, this function reads the message body and returns another object, this one containing information on the message size, message body and MIME parts within it. In order to obtain the message size, I need to simply access this object's "bytes" property.
<td valign="top"> <font face="Verdana" size="1"> <? // display message size echo ceil(($structure>bytes/1024)), " KB";
16
For greater readability, I've converted the number into kilobytes and rounded up to the nearest integer. At this point, I have absolutely no clue how to find out the attachment status. After a few experiments with the imap_fetchstructure() and imap_body() functions, I was able to obtain the complete body of the message, including the headers for MIME attachments. However, parsing these headers manually turned out to be fairly messy and codeintensive, and my gut tells me there's a better way to do it. So I'm going to leave this aside for now and come back to it after boning up on some MIME theory.
<td valign="top"> <font face="Verdana" size="1"> <? // attachment handling code goes here ?> </font> </td>
Finally, I need to provide some way for the user to delete messages from the mailbox. The traditional technique is a checkbox next to each message, which is used to select each message for deletion...and I'm a big fan of tradition.
<td align="right" valign="top"> <input type="Checkbox" name="dmsg[]" value="<? echo $x; ?>"> </td>
Note that each checkbox is linked to the message number, and that the selected message numbers will be added to the $dmsg array. When the form is submitted, the "delete.php" script (discussed next) will use this array to identify and mark messages for deletion from the server. 7. With all (or most of) the information now displayed, the last task is to clean up by closing the POP3 connection.
If you're familiar with the POP3 protocol, this is equivalent to sending a QUIT command to the server.
17
Building A PHPBased Mail Client (part 1) Here's what it all looks like:
18
<? // delete.php delete messages // session check session_start(); if (!session_is_registered("SESSION")) { header("Location: error.php?ec=2"); exit; } // open POP connection $inbox = @imap_open ("{". $SESSION_MAIL_HOST . "/pop3:110}", $SESSION_USER_NAME, $SESSION_USER_PASS) or header("Location: error.php?ec=3"); // delete specified message numbers for($x=0; $x<sizeof($dmsg); $x++) { imap_delete($inbox, $dmsg[$x]); } // clean up and go back to list page imap_close($inbox, CL_EXPUNGE); header("Location: list.php"); ?>
As always, the first step is to check for the existence of a valid session. Assuming that exists, a connection is opened to the mail server and the imap_delete() function, in combination with the contents of the $dmsg array, is used to mark messages for deletion (if you're familiar with the POP3 protocol, this is equivalent to sending a series of DELE commands to the server). It should be noted that imap_delete() merely marks messages for deletion; it does not actually remove them
19
Building A PHPBased Mail Client (part 1) from the server. In order to actually erase the marked messages, it's necessary to specify the CL_EXPUNGE argument while closing the mailbox with imap_close() (an alternative here would be to use the equivalent imap_expunge() command). Once all messages have been deleted, the browser is redirected to the message list. Deleted messages should now no longer appear in this list.
20
<? // logout.php destroy session // destroy session variables and send back to login page session_start(); session_unregister("SESSION"); session_unregister("SESSION_USER_NAME"); session_unregister("SESSION_USER_PASS"); session_unregister("SESSION_MAIL_HOST"); header("Location:index.php"); ?>
If you look at "login.php" again, you'll see that I'm simply destroying, via session_unregister(), the session variables created at login time. This is necessary to avoid having one user's sensitive account information "inherited" by subsequent users. Once the session has been destroyed, the browser is redirected back to the index page. And so the cycle continues... That's about it for this opening segment. In this article, you learned a little bit about the basics of designing software applications namely, putting down requirements on paper, separating common elements into a single location, and keeping lots of caffeine handy. You also got an introduction to PHP's IMAP functions, using builtin IMAP constructs to connect to a POP3 server and obtain a detailed message listing from it. Finally, you learned a little about PHP's session management functions, with code illustrations of how to create, use and destroy session variables. I still have a long way to go before this application is complete. My primary problem right now is understanding how to handle attachments, both so that I can display (and download) them, and so that I can attach them to new messages or replies. I plan to bone up on a little theory before attempting this come back next week and I'll tell you what I find out. Note: All examples in this article have been tested on Linux/i386 with Apache 1.3.12 and PHP 4.0.6. Examples are illustrative only, and are not meant for a production environment. Melonfire provides no warranties or support for the source code described in this article. YMMV!
21