Sunteți pe pagina 1din 38

By icarus

This article copyright Melonfire 20002002. All rights reserved.

Building A PHPBased Mail Client (part 3)

Table of Contents
The Road Ahead ..................................................................................................................................................1 Composing Yourself ............................................................................................................................................2 Return To Sender................................................................................................................................................5 Coming Forward...............................................................................................................................................12 Setting Boundaries............................................................................................................................................20 Under Construction..........................................................................................................................................27 When Things Go Wrong..................................................................................................................................32 Game Over.........................................................................................................................................................35

The Road Ahead


In the previous segment of this case study, I taught you a little bit about how MIME attachments work, and demonstrated a few functions to handle multipart MIME email. At the end of that article, you had a fullyfunctional mail reader, though not one, alas, that allowed you to actually compose, forward or reply to a message. This concluding segment will rectify that problem, enhancing the application already developed by adding support for these important functions. Additionally, it will demonstrate PHP's HTTP upload capabilities, illustrate the process of constructing a MIME message (complete with attachments) and provide links to further reading on the topic. In case you don't already have the source code for the application described in this case study, you can download it from here.

The Road Ahead

Composing Yourself
You'll remember, from our discussion of the "view.php" script, that the generated page includes a series of command buttons at the top.

<! command buttons from view.php > <! some HTML snipped out for readability > <td><a href="compose.php">Compose</a></td> <td><a href="reply.php?id=<? echo $id; ?>">Reply</a></td> <td><a href="forward.php?id=<? echo $id; ?>">Forward</a></td> <td><a href="delete.php?dmsg[]=<? echo $id; ?>">Delete</a></td> <td><a href="list.php">Messages</a></td>

The first (and simplest) of these is the "compose.php" script, which merely creates a blank form representing a new email message.

<? // compose.php compose new message // includes and session check ?> <html> <head> </head> <body bgcolor="White"> <? // page header ?> <! commands snipped > <table border="0" cellspacing="1" cellpadding="5" width="100%"> <form action="send.php" method="post" enctype="multipart/formdata"> <tr> <td valign=top><font face="Verdana" size="1"><b>From: </b></font></td> <td valign=top width=100%><input type="Text" name="from" size="30" maxlength="75" value="<? echo $SESSION_USER_NAME . "@" . $SESSION_MAIL_HOST; ?>">

Composing Yourself

Building A PHPBased Mail Client (part 3) </td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>To: </b></font></td> <td valign=top><input type="Text" name="to" size="30"></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Cc: </b></font></td> <td valign=top><input type="Text" name="cc" size="30"></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Bcc: </b></font></td> <td valign=top><input type="Text" name="bcc" size="30"></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Subject: </b></font></td> <td valign=top><input type="Text" name="subject" size="50"></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Message: </b></font></td> <td valign=top bgcolor="White"><textarea name="body" cols="60" rows="15" wrap="VIRTUAL" ></textarea></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Attachment: </b></font></td> <td valign=top bgcolor="White"><input type="file" name="attachment" size="20"></td> </tr> </form> </table> </body> </html>

Composing Yourself

Building A PHPBased Mail Client (part 3)

Here's what it looks like:

There are a couple of things to be noted here. First, the

<input type="file" name="attachment" size="20">

construct near the end of the form. This form construct creates a "Browse..." button on the form, which allows for file selection and upload through the browser; I plan to allow the user to upload message attachments though this construct. Second, note the form encoding type and method:

<form action="send.php" method="post" enctype="multipart/formdata"> <! snipped out HTML > </form>

This encoding type and method must be specified whenever you attempt file upload over HTTP. PHP's official site has a manual page devoted to the topic take a look at http://download.php.net/manual/en/features.fileupload.php and then come back for more.

Composing Yourself

Return To Sender
Next up, "reply.php", which receives a message ID from "view.php"; it uses this message ID to determine which message has been selected for replying. Stripped to its essence, this is an HTML form similar to the one you just saw, with some additional code to format the message and its headers appropriately.

<? // reply.php reply to message // includes and session checks // check for required values if (!$id) { header("Location: error.php?ec=4"); 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"); ?> <html> <head> </head> <body bgcolor="White"> <? // page header // get message headers for specified message $headers = imap_header($inbox, $id); // check for ReplyTo: header and use if available if($headers>reply_toaddress) { $to = trim($headers>reply_toaddress); } else { $to = trim($headers>fromaddress); } // check for Re: in subject header if(strtolower(substr(trim($headers>Subject), 0, 3)) == "re:") {

Return To Sender

Building A PHPBased Mail Client (part 3) $subject = $headers>Subject; } else { $subject = "Re: " . $headers>Subject; } // get message structure and parse $structure = imap_fetchstructure($inbox, $id); if(sizeof($structure>parts) > 1) { $sections = parse($structure); $attachments = get_attachments($sections); } ?> <table width="100%" border="0" cellspacing="3" cellpadding="5"> <tr> <td width="100%">&nbsp;</td> <td valign=top align=center><a href="compose.php"><img src="images/compose.gif" width=50 height=50 alt="" border="0"><br><font face="Verdana" size="2">Compose</font></a></td> <td valign=top align=center><a href="list.php"><img src="images/list.gif" width=50 height=50 alt="" border="0"><br><font face="Verdana" size="2">Messages </font></a></td> <td valign=top align=center><a href="javascript:document.forms[0].submit()"><img src="images/send.gif" width=50 height=50 alt="" border="0"><br><font face="Verdana" size="2">Send!</font></a></td> </tr> </table> <table width="100%" border="0" cellspacing="0" cellpadding="5"> <tr> <td bgcolor="#C70D11"><font size="1">&nbsp;</font></td> </tr> </table> <table border="0" cellspacing="1" cellpadding="5" width="100%"> <form action="send.php" method="POST" enctype="multipart/formdata"> <tr>

Return To Sender

Building A PHPBased Mail Client (part 3) <td valign=top><font face="Verdana" size="1"><b>From: </b></font></td> <td valign=top width=100%><input type="Text" name="from" size="30" maxlength="75" value="<? echo $SESSION_USER_NAME . "@" . $SESSION_MAIL_HOST; ?>"> </td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>To: </b></font></td> <td valign=top><input type="Text" name="to" size="30" value="<? echo $to; ?>"></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Cc: </b></font></td> <td valign=top><input type="Text" name="cc" size="30"></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Bcc: </b></font></td> <td valign=top><input type="Text" name="bcc" size="30"></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Subject: </b></font></td> <td valign=top><input type="Text" name="subject" size="50" value="<? echo $subject; ?>"></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Message: </b></font></td> <td valign=top bgcolor="White"> <textarea name="body" cols="60" rows="15" wrap="VIRTUAL" > <? // attribution line echo "\n\n\nOn " . $headers>Date . ", you wrote: \n>"; // iterate through message parts if(is_array($sections)) {

Return To Sender

Building A PHPBased Mail Client (part 3) for($x=0; $x<sizeof($sections); $x++) { // if text type, display if($sections[$x]["type"] == "text/plain" && $sections[$x]["disposition"] != "attachment") { echo str_replace("\n", "\n>", htmlspecialchars(trim(imap_fetchbody($inbox, $id, $sections[$x]["pid"])))); } } } else { echo str_replace("\n", "\n>", htmlspecialchars(trim(imap_body($inbox, $id)))); } ?> </textarea></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Attachment: </b></font></td> <td valign=top bgcolor="White"><input type="file" name="attachment" size="20"></td> </tr> </form> </table> </body> </html> <? // clean up imap_close($inbox); ?>

This is a little more complicated than "compose.php", since I need to first retrieve the original message from the mail server and then prefill the form with values sourced from that message. For example, I need to fill the To: field with the email address of the message's original sender (or the contents of the ReplyTo: header, if it exists),

Return To Sender

Building A PHPBased Mail Client (part 3) <? // check for ReplyTo: header and use if available if($headers>reply_toaddress) { $to = trim($headers>reply_toaddress); } else { $to = trim($headers>fromaddress); } ?>

insert the subject line from the original message with a "Re: " prefix (if one doesn't already exist),

<? // check for Re: in subject header if(strtolower(substr(trim($headers>Subject), 0, 3)) == "re:") { $subject = $headers>Subject; } else { $subject = "Re: " . $headers>Subject; } ?>

add an attribution line,

<? // attribution line echo "\n\n\nOn " . $headers>Date . ", you wrote: \n>"; ?>

and quote the text within the message body.

<? echo str_replace("\n", "\n>", htmlspecialchars(trim(imap_body($inbox, $id)))); ?>

Return To Sender

Building A PHPBased Mail Client (part 3) Here, too, I need to run the parse() function to look for attachments, and only display text attachments within the message body (this code snippet was previously explained in the second segment of this article).

<? // get message structure and parse $structure = imap_fetchstructure($inbox, $id); if(sizeof($structure>parts) > 1) { $sections = parse($structure); $attachments = get_attachments($sections); } // iterate through message parts if(is_array($sections)) { for($x=0; $x<sizeof($sections); $x++) { // if text type, display if($sections[$x]["type"] == "text/plain" && $sections[$x]["disposition"] != "attachment") { echo str_replace("\n", "\n>", htmlspecialchars(trim(imap_fetchbody($inbox, $id, $sections[$x]["pid"])))); } } } else { echo str_replace("\n", "\n>", htmlspecialchars(trim(imap_body($inbox, $id)))); } ?>

Finally, since users should have the ability to add attachments to a reply, the form also includes a "file" input type and a "multipart/formdata" form encoding type.

<form action="send.php" method="POST" enctype="multipart/formdata"> <! snipped out HTML > <input type="file" name="attachment" size="20">

Return To Sender

10

Building A PHPBased Mail Client (part 3) <! snipped out HTML > </form>

Here's what it looks like:

Return To Sender

11

Coming Forward
The third of this merry trio is "forward.php", which also receives a message ID from "view.php"; it uses this message ID to determine which message has been selected for forwarding. Of the three forms, "forward.php" has perhaps the most work to do. The form generated by "compose.php" is almost completely empty, while that generated by "reply.php" has only to worry about importing the correct headers and message body from the original message. The "forward.php" script, though, has to perform all the functions of "reply.php" and also handle attachments that may be embedded in the original message. Consequently, the code for "forward.php" is a hybrid what you've already seen in "reply.php" and "view.php" take a look:

<? // forward.php forward message // includes and session check // check for missing values if (!$id) { header("Location: error.php?ec=4"); exit; } $inbox = @imap_open ("{". $SESSION_MAIL_HOST . "/pop3:110}", $SESSION_USER_NAME, $SESSION_USER_PASS) or header("Location: error.php?ec=3"); ?> <html> <head> </head> <body bgcolor="White"> <? // page header // get message headers and structure $headers = imap_header($inbox, $id); $structure = imap_fetchstructure($inbox, $id); // if multipart mail, parse if(sizeof($structure>parts) > 1) { $sections = parse($structure); $attachments = get_attachments($sections); }

Coming Forward

12

Building A PHPBased Mail Client (part 3) ?> <table width="100%" border="0" cellspacing="3" cellpadding="5"> <tr> <td width="100%">&nbsp;</td> <td valign=top align=center><a href="compose.php"><img src="images/compose.gif" width=50 height=50 alt="" border="0"><br><font face="Verdana" size="2">Compose</font></a></td> <td valign=top align=center><a href="list.php"><img src="images/list.gif" width=50 height=50 alt="" border="0"><br><font face="Verdana" size="2">Messages </font></a></td> <td valign=top align=center><a href="javascript:document.forms[0].submit()"><img src="images/send.gif" width=50 height=50 alt="" border="0"><br><font face="Verdana" size="2">Send!</font></a></td> </tr> </table> <table width="100%" border="0" cellspacing="0" cellpadding="5"> <tr> <td bgcolor="#C70D11"><font size="1">&nbsp;</font></td> </tr> </table> <table border="0" cellspacing="1" cellpadding="5" width="100%"> <form action="send.php" method="POST" enctype="multipart/formdata"> <tr> <td valign=top><font face="Verdana" size="1"><b>From: </b></font></td> <td valign=top width=100%><input type="Text" name="from" size="30" maxlength="75" value="<? echo $SESSION_USER_NAME . "@" . $SESSION_MAIL_HOST; ?>"> </td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>To: </b></font></td> <td valign=top><input type="Text" name="to" size="30"></td> </tr>

Coming Forward

13

Building A PHPBased Mail Client (part 3) <tr> <td valign=top><font face="Verdana" size="1"><b>Cc: </b></font></td> <td valign=top><input type="Text" name="cc" size="30"></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Bcc: </b></font></td> <td valign=top><input type="Text" name="bcc" size="30"></td> </tr> <? // empty subject correction if ($headers>Subject == "") { $subject = "No subject"; } else { $subject = $headers>Subject; } ?>

<tr> <td valign=top><font face="Verdana" size="1"><b>Subject: </b></font></td> <td valign=top><input type="Text" name="subject" size="50" value="<? echo "Fw: " . $subject; ?>"></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Message: </b></font></td> <td valign=top bgcolor="White"> <textarea name="body" cols="60" rows="15" wrap="VIRTUAL" > <? // display message body with forward symbol > echo "\n\n"; echo ">From: $headers>fromaddress\n"; echo ">To: $headers>toaddress\n"; if($headers>ccaddress) { echo ">Cc: $headers>ccaddress\n"; } echo ">Date: $headers>Date\n"; echo ">Subject: $headers>Subject\n";

Coming Forward

14

Building A PHPBased Mail Client (part 3) // if multipart, display text parts if(is_array($sections)) { for($x=0; $x<sizeof($sections); $x++) { //if(substr($sections[$x]["type"], 0, 4) == "text") if($sections[$x]["type"] == "text/plain" && $sections[$x]["disposition"] != "attachment") { echo str_replace("\n", "\n>", htmlspecialchars(trim(imap_fetchbody($inbox, $id, $sections[$x]["pid"])))); } } } else { // else display body echo str_replace("\n", "\n>", htmlspecialchars(trim(imap_body($inbox, $id)))); } ?> </textarea></td> </tr> <tr> <td valign=top><font face="Verdana" size="1"><b>Attachments: </b></font></td> <td valign=top bgcolor="White"><input type="file" name="attachment" size="20"> <? // if attachments exist // display as list of checkboxes if (is_array($attachments)) { ?> <br> <font face="Verdana" size="1"><ul> <? for($x=0; $x<sizeof($attachments); $x++) { echo "<input type=checkbox checked name=amsg[] value=" . $attachments[$x]["pid"] . ">" . $attachments[$x]["name"] . " (" . ceil($attachments[$x]["size"]/1024) . " KB)<br>";

Coming Forward

15

Building A PHPBased Mail Client (part 3) } ?> </ul></font> <? } ?> </td> </tr> <input type="hidden" name="id" value="<? echo $id; ?>"> </form> </table> </body> </html> <? // clean up imap_close($inbox); ?>

As is routine by now, the first part of the script checks for a valid session and then opens up a connection to the user's mailbox on the POP3 server. The supplied message ID is then used to retrieve the structure and headers for the specified message, and parse() it for attachments.

<? $inbox = @imap_open ("{". $SESSION_MAIL_HOST . "/pop3:110}", $SESSION_USER_NAME, $SESSION_USER_PASS) or header("Location: error.php?ec=3"); // get message headers and structure $headers = imap_header($inbox, $id); $structure = imap_fetchstructure($inbox, $id); // if multipart mail, parse if(sizeof($structure>parts) > 1) { $sections = parse($structure); $attachments = get_attachments($sections); } ?>

Unlike "reply.php", which has to read the original message's headers and prefill the form's recipient and subject fields appropriately, "forward.php" has to display these headers within the message body itself.

Coming Forward

16

Building A PHPBased Mail Client (part 3) <? // display message body with forward symbol > echo "\n\n"; echo ">From: $headers>fromaddress\n"; echo ">To: $headers>toaddress\n"; if($headers>ccaddress) { echo ">Cc: $headers>ccaddress\n"; } echo ">Date: $headers>Date\n"; echo ">Subject: $headers>Subject\n"; ?>

Next, the same code seen previously in "reply.php" is used to isolate and print the text sections of the message.

<? // if multipart, display text parts if(is_array($sections)) { for($x=0; $x<sizeof($sections); $x++) { //if(substr($sections[$x]["type"], 0, 4) == "text") if($sections[$x]["type"] == "text/plain" && $sections[$x]["disposition"] != "attachment") { echo str_replace("\n", "\n>", htmlspecialchars(trim(imap_fetchbody($inbox, $id, $sections[$x]["pid"])))); } } } else { // else display body echo str_replace("\n", "\n>", htmlspecialchars(trim(imap_body($inbox, $id)))); } ?>

A list of the original message's attachments are also displayed, together with checkboxes which allow the user to select which ones should get forwarded.

Coming Forward

17

Building A PHPBased Mail Client (part 3) <? // if attachments exist // display as list of checkboxes if (is_array($attachments)) { ?> <br> <font face="Verdana" size="1"><ul> <? for($x=0; $x<sizeof($attachments); $x++) { echo "<input type=checkbox checked name=amsg[] value=" . $attachments[$x]["pid"] . ">" . $attachments[$x]["name"] . " (" . ceil($attachments[$x]["size"]/1024) . " KB)<br>"; } ?>

Pay attention to the checkboxes when the form is submitted, the array $amsg will contain the part IDs of the attachments selected for inclusion in the forwarded message. I'll be using this array extensively in the next script. And, just to make things interesting, how about also allowing the user to upload and add a new attachment to the forwarded message?

<form action="send.php" method="POST" enctype="multipart/formdata"> <! snipped out HTML > <input type="file" name="attachment" size="20"> <! snipped out HTML >

Here's what it all looks like:

Coming Forward

18

Building A PHPBased Mail Client (part 3)

Coming Forward

19

Setting Boundaries
Now, if you've been paying attention, you'll have noticed one rather unusual thing about the three forms you've just seen. All of them point to the same form processor, "send.php". This might seem a somewhat odd choice on my part after all, the three forms described above are all slightly different in character from each other, and you might think that writing a single processor for all three of them would be fairly complex but bear with me and you'll see how it actually streamlines the entire mail delivery process. You'll remember, from the second part of this article, how I had come up with a standard process flow to be followed while sending mail. The process involved first building the headers, then adding the message body and, if one or more attachments exist, encoding and appending them to the message body, with a unique boundary marker separating the various sections. This is exactly what "send.php" is going to do. Take a look:

<?php // send.php send message // includes and session check // ensure that at least one field has values if (!$to && !$cc && !$bcc) { header("Location: error.php?ec=6"); exit; } // check for valid file if attachment exists if ($attachment_name && $attachment_size <= 0) { header("Location: error.php?ec=7"); exit; } // append signature to body $sig = "\r\n\r\nVisit http://www.melonfire.com/community/columns/trog/ for articles\r\nand tutorials on PHP, Python, Perl, MySQL, JSP and XML\r\n"; $body .= $sig; // put all addresses into a single string if($to) { $addresses .= $to . ","; } if($cc) { $addresses .= $cc . ","; } if($bcc) { $addresses .= $bcc . ","; }

Setting Boundaries

20

Building A PHPBased Mail Client (part 3) // split address list into array $all = split(",", $addresses); // clean addresses array_walk($all, "clean_address"); // validate each address further here for($x=0; $x<sizeof($all); $x++) { if($all[$x] == "") { continue; } if(!validate_email($all[$x])) { header("Location: error.php?ec=8"); exit; } } // build message headers $headers = "From: $from\r\n"; if($cc) { $headers .= "Cc: $cc\r\n"; } if($bcc) { $headers .= "Bcc: $bcc\r\n"; } // if attachments exist if($attachment_name || sizeof($amsg) > 0) { // create a MIME boundary string $boundary = "=====MELONFIRE." . md5(uniqid(time())) . "====="; // add MIME data to the message headers $headers .= "MIMEVersion:1.0\r\n"; $headers .= "ContentType: multipart/mixed; \r\n\tboundary=\"$boundary\"\r\n\r\n"; // start building a MIME message // first part is always the message body // encode as 7bit text $str = "" . $boundary . "\r\n"; $str .= "ContentType: text/plain;\r\n\tcharset=\"usascii\"\r\n"; $str .= "ContentTransferEncoding: 7bit\r\n\r\n"; $str .= "$body\r\n\r\n";

Setting Boundaries

21

Building A PHPBased Mail Client (part 3) // if forwarded message, array $amsg[] will exist // and contain list of attachments to be forwarded if (sizeof($amsg) > 0) { // open message to be forwarded and parse it to find attachment $inbox = @imap_open ("{". $SESSION_MAIL_HOST . "/pop3:110}", $SESSION_USER_NAME, $SESSION_USER_PASS) or header("Location: error.php?ec=3"); $structure = imap_fetchstructure($inbox, $id); $sections = parse($structure); // go through attachment list and message section // if a match exists, create a MIME section and import that attachment into the message // do this as many times as there are attachments to be included for ($x=0; $x<sizeof($amsg); $x++) { for ($y=0; $y<sizeof($sections); $y++) { if($amsg[$x] == $sections[$y]["pid"]) { $data = imap_fetchbody($inbox, $id, $sections[$y]["pid"]); $str .= "" . $boundary . "\r\n"; $str .= "ContentType: " . $sections[$y]["type"] . ";\r\n\tname=\"" . $sections[$y]["name"] . "\"\r\n"; $str .= "ContentTransferEncoding: base64\r\n"; $str .= "ContentDisposition: attachment; \r\n\tfilename=\"" . $sections[$y]["name"] . "\"\r\n\r\n"; $str .= $data . "\r\n"; } } } } // if an uploaded attachment exists // encode it and attach it as a MIMEencoded section if ($attachment_name) { $fp = fopen($attachment, "rb"); $data = fread($fp, filesize($attachment)); $data = chunk_split(base64_encode($data)); fclose($fp); // add the MIME data

Setting Boundaries

22

Building A PHPBased Mail Client (part 3) $str .= "" . $boundary . "\r\n"; $str .= "ContentType: " . $attachment_type . ";\r\n\tname=\"" . $attachment_name . "\"\r\n"; $str .= "ContentTransferEncoding: base64\r\n"; $str .= "ContentDisposition: attachment; \r\n\tfilename=\"" . $attachment_name . "\"\r\n\r\n"; $str .= $data . "\r\n"; } // all done // add the final MIME boundary $str .= "\r\n$boundary\r\n"; // assign the contents of $str to $body // note that the original contents of $body will be lost $body = $str; } // send out the message if(mail($to, $subject, $body, $headers)) { $status = "Your message was successfully sent."; } else { $status = "An error occurred while sending your message."; } ?> <html> <head> </head> <body bgcolor="White"> <? $title = "Send Message"; include("header.php"); ?> <font face="Verdana" size="1"> <? echo $status; ?> <p> You can now <a href="list.php">return to the message list</a>, or <a href="compose.php">compose another message</a>. </font> </body> </html>

Setting Boundaries

23

Building A PHPBased Mail Client (part 3)

Yes, this is pretty complicated but fear not, all will be explained. 1. The first order of business is to perform certain basic checks on the data that is being submitted to "send.php". Consequently, in addition to the routine session check, I've added some code to ensure that the message has at least one recipient.

<? // ensure that at least one field has values if (!$to && !$cc && !$bcc) { header("Location: error.php?ec=6"); exit; } ?>

2. If an attachment has been uploaded, it's also a good idea to verify that the upload was successful. In order to perform these checks, I'm using the four variables created by PHP whenever a file is uploaded $attachment holds the temporary file name assigned by PHP to the uploaded file, $attachment_size is the size of the uploaded file, $attachment _type holds the MIME type, and $attachment_name holds the original name of the file (you can read about this in detail at http://download.php.net/manual/en/features.fileupload.php)

<? // check for valid file if attachment exists if ($attachment_name && $attachment_size <= 0) { header("Location: error.php?ec=7"); exit; } ?>

3. Next, I need to perform some basic validation on the addresses entered into the form. I already have a function do you remember validate_email()? to perform this validation, but I need to first clean up the addresses entered into the form and reduce them to the user@host.name form. The following code performs the address validation, redirecting the browser to the error handler if any of the addresses turn out to be invalid.

<? // put all addresses into a single string if($to) { $addresses .= $to . ","; } if($cc) { $addresses .= $cc . ","; } if($bcc) { $addresses .= $bcc . ","; }

Setting Boundaries

24

Building A PHPBased Mail Client (part 3) // split address list into array $all = split(",", $addresses); // clean addresses array_walk($all, "clean_address"); // validate each address further here for($x=0; $x<sizeof($all); $x++) { if($all[$x] == "") { continue; } if(!validate_email($all[$x])) { header("Location: error.php?ec=8"); exit; } } ?>

In case you're wondering, the clean_address() function is a tiny little function to reduce an email address to the user@host.name form, removing (among other things) whitespace, angle brackets and real names from the address text.

<? // remove extraneous stuff from email addresses // returns an email address stripped of everything but the address itself function clean_address(&$val, $index) { // clean out whitespace $val = trim($val); // look for angle braces $begin = strrpos($val, "<"); $end = strrpos($val, ">"); if ($begin !== false) { // return whatever is between the angle braces $val = substr($val, ($begin+1), $end$begin1); } } ?>

Setting Boundaries

25

Building A PHPBased Mail Client (part 3) 4. The next step is to actually create headers reflecting the recipient information.

<? // build message headers $headers = "From: $from\r\n"; if($cc) { $headers .= "Cc: $cc\r\n"; } if($bcc) { $headers .= "Bcc: $bcc\r\n"; } ?>

Assuming that no attachments exist, this is a great place to stop; all that's left to do is send the message using PHP's mail() function. But the attachment handling code is where all the meat really is and it's explained on the next page.

Setting Boundaries

26

Under Construction
In the event that attachments do exist, a couple of additional steps are required: 5. First, a unique boundary marker needs to be generated in order to separate the distinct parts of the message from each other. This boundary needs to be added to the message headers so that MIMEcompliant mail clients know where to begin and end extraction of message parts.

<? // if attachments exist if($attachment_name || sizeof($amsg) > 0) { // create a MIME boundary string // feel free to be original here! $boundary = "=====MELONFIRE." . md5(uniqid(time())) . "====="; // add MIME data to the message headers $headers .= "MIMEVersion:1.0\r\n"; $headers .= "ContentType: multipart/mixed; \r\n\tboundary=\"$boundary\"\r\n\r\n"; // start building a MIME message } ?>

Note that the term "attachment" here refers to two types of attachments: an uploaded attachment, or (only in the case of forwarded message), an attachment included from the original message. My message construction code must account for both types of attachments. 6. Next, a MIME message needs to be constructed, with the message body sent as the first section and the encoded attachments following it. Note the additional "" that has to be appended to the specified boundary within the message itself omit it and your MIME message will not be parsed correctly by a MIMEcompliant mail client (you may remember this from the second part of this article).

<? // if attachments exist if($attachment_name || sizeof($amsg) > 0) { // create a MIME boundary string // start building a MIME message // first part is always the message body // encode as 7bit text

Under Construction

27

Building A PHPBased Mail Client (part 3) $str = "" . $boundary . "\r\n"; $str .= "ContentType: text/plain;\r\n\tcharset=\"usascii\"\r\n"; $str .= "ContentTransferEncoding: 7bit\r\n\r\n"; $str .= "$body\r\n\r\n"; // handle forwarded messages } ?>

At this point, $str holds the message body. 7. Assuming that this is a forwarded message which includes attachments, I'll need to first retrieve the selected attachment(s) from the original message. This involves connecting to the mail server, retrieving the message structure, parsing it with my custom parse() function, and pulling out the textencoded attachment.

<? // if attachments exist if($attachment_name || sizeof($amsg) > 0) { // MIME message construction code // if forwarded message, array $amsg[] will exist // and contain list of attachments to be forwarded if (sizeof($amsg) > 0) { // open message to be forwarded and parse it to find attachment $inbox = @imap_open ("{". $SESSION_MAIL_HOST . "/pop3:110}", $SESSION_USER_NAME, $SESSION_USER_PASS) or header("Location: error.php?ec=3"); $structure = imap_fetchstructure($inbox, $id); $sections = parse($structure); // go through attachment list and message section // if a match exists, create a MIME section and import that attachment into the message // do this as many times as there are attachments to be included for ($x=0; $x<sizeof($amsg); $x++) { for ($y=0; $y<sizeof($sections); $y++) { if($amsg[$x] == $sections[$y]["pid"])

Under Construction

28

Building A PHPBased Mail Client (part 3) { $data = imap_fetchbody($inbox, $id, $sections[$y]["pid"]); $str .= "" . $boundary . "\r\n"; $str .= "ContentType: " . $sections[$y]["type"] . ";\r\n\tname=\"" . $sections[$y]["name"] . "\"\r\n"; $str .= "ContentTransferEncoding: base64\r\n"; $str .= "ContentDisposition: attachment; \r\n\tfilename=\"" . $sections[$y]["name"] . "\"\r\n\r\n"; $str .= $data . "\r\n"; } } } }

// handle uploaded attachments } ?>

Each included attachment is then added to the message string ($str) that is being constructed, with appropriate headers to describe the attachment type and name. 8. Next, we need some code to handle uploaded attachments (the second type). In this case, the uploaded file is in binary format and needs to be first encoded into BASE64 format before being attached to the message.

<? // if attachments exist if($attachment_name || sizeof($amsg) > 0) { // MIME message construction code // handle forwarded messages // if an uploaded attachment exists // encode it and attach it as a MIMEencoded section if ($attachment_name) { $fp = fopen($attachment, "rb"); $data = fread($fp, filesize($attachment)); $data = chunk_split(base64_encode($data)); fclose($fp); // add the MIME data $str .= "" . $boundary . "\r\n";

Under Construction

29

Building A PHPBased Mail Client (part 3) $str .= "ContentType: " . $attachment_type . ";\r\n\tname=\"" . $attachment_name . "\"\r\n"; $str .= "ContentTransferEncoding: base64\r\n"; $str .= "ContentDisposition: attachment; \r\n\tfilename=\"" . $attachment_name . "\"\r\n\r\n"; $str .= $data . "\r\n"; } } // all done ?>

The binary attachment is first converted into a textbased BASE64 representation with PHP's base64_encode() function, and then added to the message string ($str). Note the chunk_split() function, used to split the BASE64encoded text into data chunks suitable for insertion into an email message, and the HTTP upload variables created by PHP, used to add information to the ContentType: and ContentDisposition: headers. 9. With all attachments handled, the final task is to end the MIME message with the boundary marker and an additional "" at the end, signifying the end of the message.

<? // all done // add the final MIME boundary $str .= "\r\n$boundary\r\n"; // assign the contents of $str to $body // note that the original contents of $body will be lost $body = $str; ?>

10. Finally, mail() the message out and display a status message indicating whether or not the mail was accepted for delivery. Note that the Bcc: header is not correctly processed on Windows systems.

<? // send out the message if(mail($to, $subject, $body, $headers)) { $status = "Your message was successfully sent."; } else { $status = "An error occurred while sending your message."; }

Under Construction

30

Building A PHPBased Mail Client (part 3) ?>

Whew! That was complicated, but I think the effort was worth it. This script can now accept data entered into any of the three forms described previously, in addition to possessing the intelligence necessary to encode uploaded attachments or import forwarded ones. And it works like a charm try it and see for yourself!

Under Construction

31

When Things Go Wrong...


The last script an extremely simple one is the error handler, "error.php". If you look at the source code, you'll notice many links to this script, each one passing it a cryptic error code via the $ec variable. Very simply, "error.php" intercepts the variable and converts it to a humanreadable error message, which is then displayed to the user.

<? // error.php error handler switch($ec) { // login failure case 1: $message = "An error occurred while logging you in. Please verify your account information and <a href=logout.php>log in again</a>."; break; // session authentication failure case 2: $message = "An error occurred while performing your request. Please <a href=logout.php>log in again</a>."; break; // POP3 connection problem case 3: $message = "A connection could not be opened to the mail server. Please verify your account information and <a href=logout.php>log in again</a>."; break; // missing variable case 4: $message = "An error occurred while performing your request. Please <a href=logout.php>log in again</a>."; break; // email addresses absent case 6: $message = "Your message could not be processed, as it contained no valid recipient addresses. Please <a href=compose.php>try again</a>.";

When Things Go Wrong...

32

Building A PHPBased Mail Client (part 3) break; // attachment problem case 7: $message = "An error occurred while uploading the message attachment. Please <a href=compose.php>try again</a>."; break; // email addresses invalid case 8: $message = "Your message could not be processed, as it contained one or more invalid email addresses. Please <a href=compose.php>try again</a>."; break; // everything else default: $message = "An unspecified error occurred while performing your request. Please <a href=logout.php>log in again</a>."; break; } ?> <html> <head> </head> <body bgcolor="White"> <? // page header ?> <font face="Verdana" size="1"> <? echo $message; ?> </font> </body> </html>

Here's what it looks like.

When Things Go Wrong...

33

Building A PHPBased Mail Client (part 3)

Simple and elegant not to mention flexible. Found a new error? No problem assign it an error code and let "error.php" know.

When Things Go Wrong...

34

Game Over
And that's about it for this case study. We've covered a whole range of different things over the past couple of weeks session management, HTTP headers and file upload, code modularization, MIME attachments, mail server connection and message retrieval, and a whole lot more and I hope you found the process interesting and entertaining. This case study, though slightly longer than usual, also demonstrates that application development for the Web requires a great deal more than just a knowledge of PHP. In order to develop an efficient, scalable and errorfree Web application, developers must have a fundamental understanding of the protocols and design principles of the Internet, and must be able to apply this knowledge judiciously, always selecting the technology and approach that is optimal for a particular situation. This is no easy task it can take years to develop this approach but the payoff, both in terms of betterdesigned code and an overall feeling of satisfaction, is well worth it. Putting together this mail client was, at times, a frustrating exercise in trial and error, but I've come out the other end with a greater understanding of how email works, and a greater respect for the people whose job involves designing and implementing email solutions. I found the following resources invaluable during the development process if you're interested in learning more about the various topics discussed in this case, you should make it a point to visit them: The PHP manual page on POP3 functions, at http://download.php.net/manual/en/ref.imap.php The PHP manual page on session management functions, at http://download.php.net/manual/en/ref.session.php The PHP manual page on HTTP upload, at http://download.php.net/manual/en/features.fileupload.php The PHP manual page on string functions, at http://download.php.net/manual/en/ref.strings.php The PHP manual page on mail functions, at http://download.php.net/manual/en/ref.mail.php The official POP3 RFC, at http://www.faqs.org/rfcs/rfc1939.html The official SMTP RFC, at http://www.faqs.org/rfcs/rfc2821.html The official MIME RFC, at http://www.faqs.org/rfcs/rfc2045.html It should be noted at this point that this project is by no means complete. Since this prototype was introduced for internal use a few days back, a number of minor bugs have been reported, and some additional capabilities requested. Consequently, the source code provided in this article should be treated as prerelease code, unsuitable for use in production environments; I still have a ways to go before releasing this as a product to our customer. Among the features requested: the ability to upload multiple attachments at a time; to support HTMLencoded messages; to allow sorting of the message listing by name, size, date and owner; to allow for group replies; to limit the number of files displayed per page; to change the default colours; and to display the

Game Over

35

Building A PHPBased Mail Client (part 3) name of the currently loggedin user. Unusual MIME attachments also tend to result in unpredictable behaviour again, this is something that I need to debug and tune further. I plan to continue adding features and optimizing code as and when time permits if you'd like to give me a hand, or have ideas on how to improve the techniques discussed here, drop me a line and let me know. Ciao! 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!

Game Over

36

S-ar putea să vă placă și