Building A PHP-Based Mail Client (part 1)

Ever wondered how Web-based mail clients work? Find out here.

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 long-lost relatives or distant business partners. Today, email is all-pervasive - it has a user base ranging from doting grandmothers to over-enthusiastic dot-commers - 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 criss-crossing 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 Web-based 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 open-source tools like PHP, the process was simplified considerably. Over the course of this article, I'm going to demonstrate how, by building a PHP-based mail client suitable for reading and writing email in any Web browser.

The goal here is two-fold: to introduce novice and intermediate programmers to the process of designing and implementing a Web-based 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 good-looking) email solution for use on their corporate intranet or Web site.

Lofty goals, you scoff? Well, let's see...

Requiring Immediate Attention

Before we get into the nitty-gritty of syntax and structure, it's important to first put down the requirements of the software to be designed. This is a sometimes-tedious but always-necessary precedent to actual implementation of any software project, as it simultaneously offers a "big picture" view of the entire project and also provides a reference for the actual code development.

Typically, the software requirements are obtained after an analysis of the problems faced by the customer - an intensive, frequently-frustrating process involving large amounts of caffeine. In this specific case, though, I was able to arrive at the requirements after a fairly short conversation with the customer, during which the following problems became clear:

  1. Members of the customer's sales team were frequently on the road chasing down leads. During this period, they had no way of accessing their internal corporate mail. The customer was looking for a tool that would allow employees to get to their mail even if they weren't physically at the workplace.

  2. A number of the customer's employees were part-time or freelance workers, who came to the office only occasionally. Rather than assign these part-timers a dedicated computer each, the customer wanted to assign them a single "guest" machine, which could be used by them whenever they came in to work. A Web-based mail solution would be useful here too, as it would allow different users to use a single machine to read their mail.

The customer's evaluation of his problems has led him to conclude that he needed a simple Web-based mail client, along the lines of Hotmail (http://www.hotmail.com) or Mail.com (http://www.mail.com). Consequently, the brief was simple enough: a mail client which supported the standard feature set of Windows mail clients like Eudora and Microsoft Outlook, yet was accessible via a Web browser.

After a little research, I came up with the following list of software requirements (which was eventually approved by the customer):

  1. The application must be capable of connecting to any POP3-compatible mail server (IMAP support was not a requirement) and retrieving a list of messages for a user-specified mailbox on that server. This message list must display important message headers - the sender, subject and size - together with (optionally) an attachment icon.

  2. The application must be capable of displaying the contents of any message from the message list.

  3. The application must allow the user to create and send a new email message (to multiple recipients simultaneously, if required)

  4. The application must allow the user to reply to any message.

  5. The application must allow the user to forward any message, with the option to include all, some or none of the message's original attachments.

  6. The application must allow the user to delete any message from the server.

  7. The application must support mail attachments, and allow the user to download these to his local 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 full-featured 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).

Download: 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="<?php 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 <user@pop.server.name> and one for his password.

Here's what it looks like:

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:

<?php

// 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));

    // 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.

Fully Function-al

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:

<?php
// check if email address is valid
function validate_email($val)
{
    if ($val != "") {
        $pattern = "/^([a-zA-Z0-9])+([\.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-]+)+/";
        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.

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 &copy; <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><?php echo $title; ?></b></font></td>
    <td bgcolor="#C70D11" align="right"><?php 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><?php
} ?>&nbsp;</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:

<?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">

<?php
// page header
$title = "Message Listing ($total total)";
include("header.php");
?>

<table width="100%" border="0" cellspacing="3" cellpadding="5">
<!-- command buttons - snipped -->
</table>

<?php
if ($total > 0) {
    ?>
<table width="100%" border="0" cellspacing="0" cellpadding="5">
<form action="delete.php" method="post">
<!-- message info columns -->
<tr>
    <td width="5%"><font size="-1">&nbsp;</font></td>
    <td width="5%"><font size="-1">&nbsp;</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="<?php echo $x; ?>">
</td>
<td valign="top">
<?php
// attachment handling code goes here
?>
</td>
<td valign="top">
<font face="Verdana" size="-1"><?php echo substr($headers->Date, 0, 22); ?></font>
</td>
<td valign="top">
<font face="Verdana" size="-1"><?php echo htmlspecialchars($headers->fromaddress); ?></font>
</td>
<td valign="top">
<font face="Verdana" size="-1">
<a href="view.php?id=<?php echo $x; ?>">
<?php
    // correction for empty subject
    if ($headers->Subject == "") {
        echo "No subject";
    } else {
        echo $headers->Subject;
    } ?>
</a>
</font>
</td>
<td valign="top">
<font face="Verdana" size="-1">
<?php
// display message size
echo ceil(($structure->bytes/1024)), " KB"; ?>
</font>
</td>
</tr>
<?php
}
    // clean up
    imap_close($inbox); ?>
</form>
</table>
<?php
} 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).
<?php
// includes

// 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.

  1. 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).
<?php
// 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.

  1. 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.
<?php
// get number of messages
$total = imap_num_msg($inbox);

// page header
$title = "Message Listing ($total total)";
include("header.php");
?>

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.

  1. 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.
<?php
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>
<?php
} 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.

<?php
// 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
}
?>

If you're familiar with the POP3 protocol, this is equivalent to sending a series of RETR commands to the server.

Treating Messages As Objects

  1. Of the headers I've selected for display, the sender, subject and date are fairly easy to obtain - the imap_header() function returns an object, one for each message, exposing these values as properties. All I need to do is access these properties and echo() them to the page. For example, the object property
$obj->fromaddress

would reference the message's From: header, while the property

$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.imap-header.php

Here's the code to print the message date and sender:

<!-- snip -->
<td valign="top">
<font face="Verdana" size="-1"><?php echo substr($headers->Date, 0, 22); ?></font>
</td>
<td valign="top">
<font face="Verdana" size="-1"><?php 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=<?php echo $x; ?>">
<?php
    // 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?

  1. 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">
<?php
// display message size
echo ceil(($structure->bytes/1024)), " KB";
?>
</font>
</td>

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 code-intensive, 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">
<?php
// 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="<?php 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.

  1. With all (or most of) the information now displayed, the last task is to clean up by closing the POP3 connection.
<?php
// clean up
imap_close($inbox);
?>

If you're familiar with the POP3 protocol, this is equivalent to sending a QUIT command to the server.

Here's what it all looks like:

Calling The Exterminator

You'll remember, from the previous pages, that every message in the message list has a checkbox accompanying it. This box, if checked, indicates that the message is to be deleted from the mail server.

If you take a close look at "list.php", you'll see that every checkbox is associated with the $dmsg array; once the form is submitted, this array will contain the numbers of all the messages selected for deletion. The "delete.php" script then needs only to connect to the mail server and iterate through this array, marking the specified messages for deletion.

Yes, it really is as simple as it sounds - take a look at the script:

<?php

// 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 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.

Back To Square One

Now, that entire effort was a pretty major exercise - especially if, like me, you hadn't done it before. And I'm not done yet - I still need to figure out how to handle attachments. But before I go there, I'd like to close up this first part with a look at, appropriately enough, the "logout.php" script.

You'll remember, from a couple pages back, that the page header includes some code to display a link to log out of the system. This link points to "logout.php", an extremely simple script which destroys the session created at the time of logging in, and sends the browser back to the application's index page.

Take a look:

<?php
// 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 built-in 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/ig86 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!

This article was first published on07 Dec 2001.