23
By icarus This article copyright Melonfire 2000-2002. All rights reserved.

[Developer Shed Network] Server Side - PHP - Building a PHP-Based Mail Client (Part 1)

  • Upload
    eminemb

  • View
    34

  • Download
    0

Embed Size (px)

Citation preview

By icarus

This article copyright Melonfire 2000−2002. All rights reserved.

Table of ContentsSimply M@gical..................................................................................................................................................1

Requiring Immediate Attention.........................................................................................................................2

Start Me Up.........................................................................................................................................................4

Fully Function−al................................................................................................................................................7

Opening Up..........................................................................................................................................................8

Treating Messages As Objects.........................................................................................................................15

Calling The Exterminator................................................................................................................................19

Back To Square One.........................................................................................................................................21

Building A PHP−Based Mail Client (part 1)

i

Simply M@gicalIt's almost hard to believe that, up until a few years ago, putting pen to paper was still the most commonmethod 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, thefastest, most efficient way to communicate. Arthur C. Clarke once said that any sufficiently advancedtechnology was indistinguishable from magic; with millions of messages criss−crossing the globe at any giventime, 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 myfirst email account, I've always found there to be something magical about the process by which a textmessage is encoded and bounced around the world from one mail server to another until it reaches itsrecipient, thence to be decoded back into its original form and displayed. And so, when I was offered theopportunity to work on a Web−based email client a few weeks back, I jumped at it; here, at last, was mychance 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 thehelp of powerful open−source tools like PHP, the process was simplified considerably. Over the course of thisarticle, I'm going to demonstrate how, by building a PHP−based mail client suitable for reading and writingemail in any Web browser.

The goal here is two−fold: to introduce novice and intermediate programmers to the process of designing andimplementing a Web−based application, with special reference to PHP's mail functions, and to offer roadwarriors, 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...

Simply M@gical 1

Requiring Immediate AttentionBefore we get into the nitty−gritty of syntax and structure, it's important to first put down the requirements ofthe software to be designed. This is a sometimes−tedious but always−necessary precedent to actualimplementation of any software project, as it simultaneously offers a "big picture" view of the entire projectand 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 − anintensive, frequently−frustrating process involving large amounts of caffeine. In this specific case, though, Iwas able to arrive at the requirements after a fairly short conversation with the customer, during which thefollowing 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 wouldallow 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 onlyoccasionally. Rather than assign these part−timers a dedicated computer each, the customer wanted to assignthem a single "guest" machine, which could be used by them whenever they came in to work. A Web−basedmail solution would be useful here too, as it would allow different users to use a single machine to read theirmail.

The customer's evaluation of his problems has led him to conclude that he needed a simple Web−based mailclient, 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 ofWindows 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 eventuallyapproved by the customer):

1. The application must be capable of connecting to any POP3−compatible mail server (IMAP support wasnot a requirement) and retrieving a list of messages for a user−specified mailbox on that server. This messagelist must display important message headers − the sender, subject and size − together with (optionally) anattachment 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 recipientssimultaneously, 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 ofthe 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

Requiring Immediate Atten... 2

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 theseactions. Note that the list above is somewhat abridged − the actual requirements document was a bit moredetailed, and included some additional items that will not be discussed here − but it still has enough materialto 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 implementationpoint of view and for other, related activities. Once the requirements are written down and approved by thecustomer, the developer can begin thinking about how to design and code the application, the interfacedesigner can begin work on the application's user interface, and the QA team can begin building test cases toverify the final release of the code.

Building A PHP−Based Mail Client (part 1)

Requiring Immediate Atten... 3

Start Me UpWith the requirements clearly defined, it's time to actually start writing some code. Since I'm a big fan ofPHP, I plan to use that as my weapon of choice during this exercise. My natural inclination towards PHP isfurther influenced by the fact that PHP comes with a full−featured set of commands for working with IMAPand 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 (youwill 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 mailserver.

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">Emailaddress</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 4

Now, this script is actually split into two parts: the first part displays the login form above, while the secondpart processes the data entered into it. An "if" loop, keyed against the presence of the $submit variable, is usedto 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));

Building A PHP−Based Mail Client (part 1)

Start Me Up 5

// store the details in session variablessession_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 pageheader("Location: list.php");}?>

The first order of business is to verify that all the information required to connect to the mail server has beenentered by the user; this information includes a valid email address and password. Assuming that both arepresent, 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 variableswith 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 aPOP3 connection and retrieve a list of messages in the user's mailbox. This redirection is accomplished bysending 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 tothe browser. Even something as minor as whitespace or a carriage return outside the PHP tags can cause thesecalls to barf all over your script.

Building A PHP−Based Mail Client (part 1)

Start Me Up 6

Fully Function−alBefore 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 thatmight come in useful elsewhere in the application, I move it into "functions.php" and include that file in myscript.

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 validfunction 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 addresssupplied conforms to the specified pattern. The function returns true or false depending on whether or not thematch was successful.

Fully Function−al 7

Opening UpWith the session instantiated, the next step is to retrieve and display a list of messages from the user's mailboxon the mail server. This is accomplished via "list.php", a PHP script which opens a connection to the POP3server, 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 userinterface. If you look at the sample screenshots in this article, you'll see that every page generated through thisapplication has some common elements: the logo at the top left corner, the copyright note at the top rightcorner, 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 codein 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; <ahref="http://www.melonfire.com/">Melonfire</a>, 2001.<br>Allrightsreserved.</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">LogOut</a></b></font><? } ?>&nbsp;</td></tr></table>

Again, by separating common interface elements into separate files, I've made it easier to customize the lookof 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'llsee examples of this over the next few pages. Note also the link to log out, which appears only after the userhas 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 8

<?php

// list.php − display message list

// includesinclude("functions.php");

// session checksession_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>

<?if ($total > 0){?><table width="100%" border="0" cellspacing="0"cellpadding="5">

Building A PHP−Based Mail Client (part 1)

Opening Up 9

<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 messagesfor($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"><? echohtmlspecialchars($headers−>fromaddress); ?></font></td><td valign="top"><font face="Verdana" size="−1"><a href="view.php?id=<? echo $x; ?>"><?// correction for empty subject

Building A PHP−Based Mail Client (part 1)

Opening Up 10

if ($headers−>Subject == ""){echo "No subject";}else{echo $headers−>Subject;}?></a></font></td><td valign="top"><font face="Verdana" size="−1"><?// display message sizeecho ceil(($structure−>bytes/1024)), " KB";?></font></td></tr><?}// clean upimap_close($inbox);?></form></table><?}else{echo "<font face=Verdana size=−1>You have no mail at thistime</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, andtested for the presence of a valid session (and, by implication, the presence of a mail username, password andhost).

<?// includes

Building A PHP−Based Mail Client (part 1)

Opening Up 11

// session checksession_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 astandard 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. PHPoffers the imap_open() function for this purpose; it accepts three parameters: the POP3 server name, the POP3user name and the corresponding password (you can also use the imap_open() command to open a connectionto 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 theserver.

If the connection is successful, this function returns a handle representing the mailbox, required for allsubsequent 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 returnedby imap_open(), is used to obtain the total number of messages in the mailbox; this number is then displayedin the page title.

<?// get number of messages$total = imap_num_msg($inbox);

// page header$title = "Message Listing ($total total)";

Building A PHP−Based Mail Client (part 1)

Opening Up 12

include("header.php");?>

Incidentally, don't be fooled by the prefix on all these function names; as stated previously, though everyfunction 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 formessage 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 thistime</font>";}?>

Within the table, a "for" loop iterates as many times as there are messages, retrieving the headers and structureof each message with the imap_header() and imap_fetchstructure() functions respectively. Throughout thisloop, the variable $x references the current message number. Note that I'm iterating through the message listin reverse order so that the more recent messages are displayed first.

<?// iterate through messagesfor($x=$total; $x>0; $x−−){// get header and structure$headers = imap_header($inbox, $x);$structure = imap_fetchstructure($inbox, $x);

// table rows here

Building A PHP−Based Mail Client (part 1)

Opening Up 13

}?>

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

Building A PHP−Based Mail Client (part 1)

Opening Up 14

Treating Messages As Objects5. Of the headers I've selected for display, the sender, subject and date are fairly easy to obtain − theimap_header() function returns an object, one for each message, exposing these values as properties. All Ineed 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 differentattribute 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 athttp://www.php.net/manual/en/function.imap−header.php

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

<!−− snip −−><td valign="top">

Treating Messages As Obje... 15

<font face="Verdana" size="−1"><? echo substr($headers−>Date,0, 22);?></font></td><td valign="top"><font face="Verdana" size="−1"><? echohtmlspecialchars($headers−>fromaddress); ?></font></td><!−− snip −−>

I also need to link each message to a script, "view.php", which displays the complete message body. I'vedecided to do this by attaching a hyperlink to the subject of every message in the message list and passing itthe 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 subjectif ($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 − themessage 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 andmessage number as arguments, this function reads the message body and returns another object, this onecontaining information on the message size, message body and MIME parts within it. In order to obtain themessage size, I need to simply access this object's "bytes" property.

<td valign="top"><font face="Verdana" size="−1"><?// display message sizeecho ceil(($structure−>bytes/1024)), " KB";

Building A PHP−Based Mail Client (part 1)

Treating Messages As Obje... 16

?></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 theimap_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 fairlymessy and code−intensive, and my gut tells me there's a better way to do it. So I'm going to leave this asidefor 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 traditionaltechnique is a checkbox next to each message, which is used to select each message for deletion...and I'm abig 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 beadded to the $dmsg array. When the form is submitted, the "delete.php" script (discussed next) will use thisarray 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 POP3connection.

<?// clean upimap_close($inbox);?>

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

Building A PHP−Based Mail Client (part 1)

Treating Messages As Obje... 17

Here's what it all looks like:

Building A PHP−Based Mail Client (part 1)

Treating Messages As Obje... 18

Calling The ExterminatorYou'll remember, from the previous pages, that every message in the message list has a checkboxaccompanying 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; oncethe 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 thespecified messages for deletion.

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

<?

// delete.php − delete messages

// session checksession_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 numbersfor($x=0; $x<sizeof($dmsg); $x++){imap_delete($inbox, $dmsg[$x]);}

// clean up and go back to list pageimap_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 isopened to the mail server and the imap_delete() function, in combination with the contents of the $dmsgarray, is used to mark messages for deletion (if you're familiar with the POP3 protocol, this is equivalent tosending 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

Calling The Exterminator 19

from the server. In order to actually erase the marked messages, it's necessary to specify the CL_EXPUNGEargument while closing the mailbox with imap_close() (an alternative here would be to use the equivalentimap_expunge() command).

Once all messages have been deleted, the browser is redirected to the message list. Deleted messages shouldnow no longer appear in this list.

Building A PHP−Based Mail Client (part 1)

Calling The Exterminator 20

Back To Square OneNow, that entire effort was a pretty major exercise − especially if, like me, you hadn't done it before. And I'mnot done yet − I still need to figure out how to handle attachments. But before I go there, I'd like to close upthis 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 logout of the system. This link points to "logout.php", an extremely simple script which destroys the sessioncreated at the time of logging in, and sends the browser back to the application's index page.

Take a look:

<?// logout.php − destroy session

// destroy session variables and send back to login pagesession_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 sessionvariables 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 cyclecontinues...

That's about it for this opening segment. In this article, you learned a little bit about the basics of designingsoftware applications − namely, putting down requirements on paper, separating common elements into asingle 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 tocreate, use and destroy session variables.

I still have a long way to go before this application is complete. My primary problem right now isunderstanding how to handle attachments, both so that I can display (and download) them, and so that I canattach them to new messages or replies. I plan to bone up on a little theory before attempting this − come backnext 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 nowarranties or support for the source code described in this article. YMMV!

Back To Square One 21