Building A PHP-Based Mail Client (part 2)

Integrate attachment handling features into the primitive mail client developed earlier.

The Plan

In the first part of this article, I demonstrated the basics of PHP's IMAP functions, using them to connect to a POP3 server and retrieve a list of messages from it. I also explained how to extract important message headers, showed you how to delete selected messages from the server, and gave you a crash course in PHP's session management functions.

While I have a fairly clear idea of the requirements for this application, there's still an important hole in my overall plan - I have not yet written any code to handle message attachments. This is holding up my implementation, as a number of scripts in the application will require this capability. And so, my first task today must be to understand how message attachments work, and write a few generic functions that allow easy manipulation of these attachments.

Let's get started!

A Picture Is Worth A Thousand Words

The best way to understand how message attachments work is with an example. Consider the following email message, which contains no attachments,

Return-Path: <jane@some.domain.com>
Received: from localdomain (unknown [205.89.123.112])
    by mail.domain.com (Postfix) with ESMTP id B0AYBHA41
    for <john@some.domain.com>; Thu,  6 Dec 2001 21:05:51 +0530 (IST)
Message-Id: <3.3.7.32.20011206210943.00b26d40@melonfire.com>
Date: Thu, 06 Dec 2001 21:09:43 +0500
To: John Doe <john@some.domain.com>
From: Jane Doe <jane@some.domain.com>
Subject: Photos
Mime-Version: 1.0
Content-Type: text/plain; charset="us-ascii"

Do you still have those vacation photos with you? Send me a copy if you do.

- Jane

and then contrast it with this one, which does.

Return-Path: <john@some.domain.com>
Received: from localdomain (unknown [205.89.123.112])
    by mail.domain.com (Postfix) with ESMTP id B0AYBHA41
    for <jane@some.domain.com>; Thu,  6 Dec 2001 21:05:51 +0530 (IST)
Message-Id: <3.0.3.32.20011207082225.006e927c@localhost>
Date: Fri, 07 Dec 2001 08:22:25 +0500
To: Jane Doe <jane@some.domain.com>
From: John Doe <john@some.domain.com>
Subject: Re: Photos
Mime-Version: 1.0
Content-Type: multipart/mixed; boundary="=====================_1007675545==_"

--=====================_1007675545==_
Content-Type: text/plain; charset="us-ascii"

Hi Jane,

Find photos attached.

John
--=====================_1007675545==_
Content-Type: application/zip; name="photos1.zip";
 x-mac-type="705A4950"; x-mac-creator="705A4950"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="photos1.zip"

UEsDBBQAAAAIABythCvu1mnpugIAALsIAAALAAAAY29tcG9zZS5waHC1lltP2zAUx59Xqd/
-- snip --
AK6LAAAAAA==
--=====================_1007675545==_
Content-Type: application/zip; name="photos2.zip";
 x-mac-type="705A4950"; x-mac-creator="705A4950"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="photos2.zip"

UEsDBBQAAAAIAACRDSux3m3PFwIAABsIAAAXAAAAc29hcGxpYi5zb2FwaW50ZXJvcC5waHC
-- snip --
cFBLBQYAAAAACwALALcCAABRfQAAAAA=
--=====================_1007675545==_--

As you can see, the message containing attachments differs from the plaintext message in a couple of important ways.

  1. Since it consists of different parts (one for the body and one for each attachment), it's Content-Type: header specifies the message type as "multipart/mixed".
Content-Type: multipart/mixed;
  1. The various message sections are demarcated by a boundary "marker"; this marker is unique to each message, and is defined in the main message header so that MIME-compliant mail clients can distinguish between the various message parts.
Content-Type: multipart/mixed; boundary="=====================_1007675545==_"

If you look at the example above, you'll see that, as specified in the message header, the boundary marker "=====================1007675545==" is used as a separator between the different parts of the message.

  1. Since the SMTP protocol cannot handle binary data, binary attachments - in this case, zip archives - must be encoded into a text representation using one of a number of different encoding methods. This information, together with information on the attachment type and original filename, appears in the header for each message section.
Content-Type: application/zip; name="photos2.zip";
 x-mac-type="705A4950"; x-mac-creator="705A4950"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="photos2.zip"

This provides the mail client with all the information it needs to decode and display the attachment. It can use the Content-Transfer-Encoding: header to determine how best to decode the text block back into binary data, the filename to determine what the resulting file should be called, and the Content-Disposition: header to decide whether the binary data should be displayed as an attachment or within the message body.

Note that this discussion is restricted to MIME attachments only. Other standards to handle mail attachments do exist. Take a look at http://www.faqs.org/rfcs/rfc2045.html for the MIME specification.

The Way Things Work

After a little research, it quickly becomes apparent that a standard process flow can be developed for attachment handling, both while sending and reading mail. Here's my first draft of the process:

When reading email,

  1. Check the message's Content-Type: header and count the number of message parts.

If the Content-Type: header indicates that the message is in plaintext, you can skip ahead to the last step. If it's anything but "text/plain", it's a fair bet that you'll have to parse it further.

An alternative here would be to parse the message body and count how many parts it's divided into - if it's two or more, you can again expect to have to parse it further.

  1. Parse the message body.

If the message is a multipart message - for example, the type "multipart/mixed" - it is necessary to parse the various sections of the message to determine the attachment attributes.

As explained previously, every message section includes a header providing information on the data enclosed within that section. This data is used by the mail client to figure out how to handle that particular section.

  1. Display the message section(s).

Once the client has sufficient information on the number and type of attachments, the final step is to display each section. Plaintext sections can be displayed as is; encoded sections may need to be decoded and then displayed. If the client supports HTML-encoded mail - as most Web-based clients do - it may need to jump through a few additional hoops to decode and render embedded HTML markup.

It's important to note that different mail clients handle multipart messages differently. Some clients display each section as is (raw text) without processing it further; others use the encoding information present in the section header to decode and display each section separately. Some clients display each message section as an attachment, while others use the data type of each section to decide whether the decoded section should be displayed inline (within the message body) or as an attachment.

What about sending email? Well, that's also fairly simple - all you need to do is build a message incrementally, with each part representing an attachment and a boundary marker separating the various parts.

  1. Build the message headers.

When sending email, the first step is to build the message headers. Some of these headers are built in response to user input - for example, the subject line and recipient addresses - while others must be added by the client itself. Typically, if one or more message attachments exist, the client will need to alter the Content-Type: of the message and generate a boundary marker to demarcate the various message sections.

  1. Add the message body and attachments.

Once the headers have been built, the next step is to add the message text. If attachments exist, they must be encoded into plaintext and appended to the message body, with the message's unique boundary marker separating the various sections from each other.

  1. Send the message.

With the headers and body both ready, the final task is to actually connect to a mail server and send the mail out. Some programming languages (like PHP) offer high-level constructs to perform this function; others require you to use low-level constructs to open up a socket connection to the server and send the message using socket communication techniques.

Structure And Syntax

Using this process flow as reference, let's now begin implementing the code for the mail client.

A quick look at the PHP manual reveals that PHP provides a number of useful functions to assist in this process:

  1. imap_fetchstructure() - read and return information on message structure

  2. imap_header() - return an object containing header elements

  3. imap_body() - retrieve the complete message body

  4. imap_fetchbody() - retrieve a specific section of the body

Of these, you've already seen the imap_fetchstructure() function in action - using a mailbox handle and message number as arguments, this function reads the message body and returns an object containing information on the message size, message body and MIME parts within it.

This object exposes a "parts" property, which is itself an array; the elements of this array are objects representing the various sections of the message. Therefore, it's possible to identify whether or not a message contains multiple parts, and obtain information on those parts, simply by iterating through this array.

Each of the objects in the "parts" array provides information on the corresponding message section - the encoding, the size, the type and subtype, the filename and so on. A complete list of the properties exposed by each of these objects is available in the PHP manual at http://www.php.net/manual/en/function.imap-fetchstructure.php - you should look at it before proceeding further.

Using this information, it's possible to write a couple of simple functions that build on imap_fetchstructure() to deliver customized information about message sections:

<?php
// define some constants
// message types
$type = array("text", "multipart", "message", "application", "audio", "image", "video", "other");
// message encodings
$encoding = array("7bit", "8bit", "binary", "base64", "quoted-printable", "other");

// parse message body
function parse($structure)
{
    global $type;
    global $encoding;

    // create an array to hold message sections
    $ret = array();

    // split structure into parts
    $parts = $structure->parts;

    for($x=0; $x<sizeof($parts); $x++)
    {
        $ret[$x]["pid"] = ($x+1);

        $this = $parts[$x];

        // default to text
        if ($this->type == "") { $this->type = 0; }
        $ret[$x]["type"] = $type[$this->type] . "/" . strtolower($this->subtype);

        // default to 7bit
        if ($this->encoding == "") { $this->encoding = 0; }
        $ret[$x]["encoding"] = $encoding[$this->encoding];

        $ret[$x]["size"] = strtolower($this->bytes);

        $ret[$x]["disposition"] = strtolower($this->disposition);

        if (strtolower($this->disposition) == "attachment")
        {
            $params = $this->dparameters;
            foreach ($params as $p)
            {
                if($p->attribute == "FILENAME")
                {
                $ret[$x]["name"] = $p->value;
                break;
                }
            }
        }
}

return $ret;
}
?>

The parse() function accepts an object, as returned by imap_fetchstructure(), and parses it to produce an array holding some very specific information on each message part.

First, parse() creates an empty array named $ret, which will ultimately contain as many elements as there are message parts.

<?php
    // create an array to hold message sections
    $ret = array();
?>

Every element of $ret will itself be an associative array, with keys representing the part number, part type, encoding, disposition, size and filename.

Next, parse() creates an array named $parts, to hold the object array returned by the "parts" property of the imap_fetchstructure() object.

<?php
    // split structure into parts
    $parts = $structure->parts;
?>

parse() then iterates through $parts, adding each element to $ret and creating keys to represent the characteristics of each part found.

<?php
    for($x=0; $x<sizeof($parts); $x++)
    {
        $ret[$x]["pid"] = ($x+1);

        $ret[$x]["type"] = $type[$this->type] . "/" . strtolower($this->subtype);

        $ret[$x]["encoding"] = $encoding[$this->encoding];

        $ret[$x]["size"] = strtolower($this->bytes);

        $ret[$x]["disposition"] = strtolower($this->disposition);

        // some parts snipped for brevity
}
?>

The IMAP specification (available at http://www.faqs.org/rfcs/rfc2060.html) defines a number, or part ID, for every part of a multipart message, starting from 1 (which is usually the message text itself); my array defines this part ID via the "pid" key, and I'm recording it at this stage itself because I'm sure to need it when handling messages containing more than one attachment.

The type and encoding of each part are stored as integers by imap_fetchstructure(); these correspond to elements of the $type and $encoding arrays respectively. If you look at the complete function definition above, you'll see that these arrays are defined outside the function, and imported into it with the "global" keyword.

Here's an example of the array returned by parse():

Array
(
    [0] => Array
        (
            [pid] => 1
            [type] => text/plain
            [encoding] => 7bit
            [size] => 41
            [disposition] =>
        )

    [1] => Array
        (
            [pid] => 2
            [type] => application/zip
            [encoding] => base64
            [size] => 50634
            [disposition] => attachment
            [name] => photos1.zip
        )

    [2] => Array
        (
            [pid] => 3
            [type] => application/zip
            [encoding] => base64
            [size] => 44882
            [disposition] => attachment
            [name] => photos2.zip
        )

)

As you can see, this is a fairly clear representation of the various sections that make up a MIME message.

Getting Attached

I've also written another function to extract the attachments only from the array returned by parse() - take a look at the get_attachments() function:

<?php
// iterate through object returned by parse()
// create a new array holding information only on message attachments
function get_attachments($arr)
{
    for($x=0; $x<sizeof($arr); $x++)
    {
        if($arr[$x]["disposition"] == "attachment")
        {
        $ret[] = $arr[$x];
        }
    }
    return $ret;
}
?>

Let's now backtrack a little and add this code to the message listing, "list.php", where it will be used to identify which messages have attachments (so that an attachment icon can be displayed next to those messages).

Here's the revised version of that 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
?>

<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

// parse structure to see if attachments exist
// display icon if so
$sections = parse($structure);
$attachments = get_attachments($sections);
if(is_array($attachments))
{
    echo "<img alt=\"Message has attachment\" src=images/list.gif width=30 height=30 border=0>";
}
else
{
    echo "&nbsp;";
}
?>
</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>

And here's what the listing looks like, after making the addition:

Room With A View

The next step, obviously, is to write a script that displays the complete contents of a particular message. You'll remember from the first part of this article that this script is called "view.php", and that it's passed a message number via the URL GET method from "list.php".

I've delayed writing this script for a while, primarily because I wasn't too comfortable with attachment handling. Now that I have a method to handle attachments, I'm confident that it shouldn't give me too much trouble...and, as it turns out, it doesn't. Take a look:

<?php
// view.php - display message

// includes

// session check

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

<?php
// get message headers and structure
$headers = imap_header($inbox, $id);
$structure = imap_fetchstructure($inbox, $id);

// if multipart, parse
if(sizeof($structure->parts) > 1)
{
    $sections = parse($structure);
    $attachments = get_attachments($sections);
}
?>

<table width="100%" border="0" cellspacing="3" cellpadding="5">
<!-- command buttons -->
<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="reply.php?id=<?php echo $id; ?>"><img src="images/reply.gif" width=50 height=50 alt="" border="0"><br><font face="Verdana" size="-2">Reply</font></a></td>
    <td valign=top align=center><a href="forward.php?id=<?php echo $id; ?>"><img src="images/forward.gif" width=50 height=50 alt="" border="0"><br><font face="Verdana" size="-2">Forward</font></a></td>
    <td valign=top align=center><a href="delete.php?dmsg[]=<?php echo $id; ?>"><img src="images/delete.gif" width=50 height=50 alt="" border="0"><br><font face="Verdana" size="-2">Delete</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>
</tr>
</table>

<table border="0" cellspacing="1" cellpadding="5" width="100%">

<tr>
<td valign=top><font face="Verdana" size="-1"><b>From: </b></font></td>
<td valign=top width=100%><font face="Verdana" size="-1"><?php echo htmlspecialchars($headers->fromaddress);
?></font></td>
</tr>

<tr>
<td valign=top><font face="Verdana" size="-1"><b>To:</b></font></td>
<td valign=top><font face="Verdana" size="-1"><?php echo htmlspecialchars($headers->toaddress); ?>
</font>
</td>
</tr>

<?php
if($headers->ccaddress)
{
?>
<tr>
<td valign=top><font face="Verdana" size="-1"><b>Cc: </b></font></td>
<td valign=top><font face="Verdana" size="-1"><?php echo htmlspecialchars($headers->ccaddress); ?>
</tr>
<?php
}
?>

<tr>
<td valign=top><font face="Verdana" size="-1"><b>Date: </b></font></td>
<td valign=top><font face="Verdana" size="-1"><?php echo $headers->Date;?></font></td>
</tr>

<tr>
<td valign=top><font face="Verdana" size="-1"><b>Subject: </b></font></td>
<td valign=top><font face="Verdana" size="-1">
<?php
// correction for empty subject
if ($headers->Subject)
{
echo $headers->Subject;
}
else
{
echo "No subject";
}
?>
</font></td>
</tr>

<tr>
<td colspan=2 valign="TOP"><pre><font face="Verdana">
<?php
// if multipart, display text sections
if(is_array($sections))
{
    for($x=0; $x<sizeof($sections); $x++)
    {
        if(($sections[$x]["type"] == "text/plain" || $sections[$x]["type"] == "message/rfc822") && $sections[$x]["disposition"] != "attachment")
        {
        echo htmlspecialchars(stripslashes(trim(imap_fetchbody($inbox, $id, $sections[$x]["pid"]))));
        echo "<br>";
        }
    }
}
else
{
echo htmlspecialchars(stripslashes(trim(imap_body($inbox, $id))));
}
?>
</font></pre></td>
</tr>

<?php
// if attachments exist
if (is_array($attachments))
{
?>
<tr>
<td valign=top><font face="Verdana" size="-1"><b>Attachments: </b></font></td>
<td valign=top><font face="Verdana" size="-1"><ul>
<?php
    // display as list
    for($x=0; $x<sizeof($attachments); $x++)
    {
        echo "<li><a href=download.php?id=$id&pid=" . $attachments[$x]["pid"] . ">" . $attachments[$x]["name"] . " (" . ceil($attachments[$x]["size"]/1024) . " KB)</a>";
    }

?>

</ul></font></td>
</tr>
<?php
}
?>

</table>

<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
    <td width="50%" align="left" valign="top">
    <?php
    if ($id > 1)
    {
        // display previous message link
        echo "<a href=view.php?id=" . ($id-1) ."><font face=Verdana size=-2>Previous message</font></a>";
    }
    ?>&nbsp;
    </td>
    <td align="right" width="50%" valign="top">
    <?php
    // display next message link
    if ($id < $total)
    {
        echo "<a href=view.php?id=" . ($id+1) ."><font face=Verdana size=-2>Next message</font></a>";
    }
    ?>&nbsp;
    </td>
</tr>
</table>

</body>
</html>

<?php
// clean up
imap_close($inbox);
?>

This is a little complicated, so I'll explain it in detail. The first step - after performing the now-routine session and value checks - is to open a connection to the POP3 server

<?php
// open POP connection
$inbox = @imap_open ("{". $SESSION_MAIL_HOST . "/pop3:110}", $SESSION_USER_NAME, $SESSION_USER_PASS) or header("Location: error.php?ec=3");

// get total messages
// wondering why I need this? keep reading...
$total = imap_num_msg($inbox);
?>

This connection is then used to retrieve the headers and structure of the specified message.

<?php
// get message headers and structure
$headers = imap_header($inbox, $id);
$structure = imap_fetchstructure($inbox, $id);
?>

Assuming that the message has more than one part, I run my custom parse() function on it.

<?php
// if multipart, parse
if(sizeof($structure->parts) > 1)
{
    $sections = parse($structure);
    $attachments = get_attachments($sections);
}
?>

Much HTML code follows - this is very similar to the code used in "list.php" to display the message headers, so I'm not going to repeat the explanation here. The interesting part comes at the tail end, when it's time to display the body.

<tr>
<td colspan=2 valign="TOP"><pre><font face="Verdana">
<?php
// if multipart, display text sections
if(is_array($sections))
{
    for($x=0; $x<sizeof($sections); $x++)
    {
        if(($sections[$x]["type"] == "text/plain" || $sections[$x]["type"] == "message/rfc822") && $sections[$x]["disposition"] != "attachment")
        {
        echo htmlspecialchars(stripslashes(trim(imap_fetchbody($inbox, $id, $sections[$x]["pid"]))));
        echo "<br>";
        }
    }
}
else
{
echo htmlspecialchars(stripslashes(trim(imap_body($inbox, $id))));
}
?>
</font></pre></td>
</tr>

In this case, I'm first checking the $sections variable returned by parse() to see if the message is, indeed, a multipart message. If it's not, I'm using the imap_body() function to display the complete body of the message (refer to the standard process flow a few pages back if this doesn't make sense to you). If, on the other hand, it does contain multiple parts, I'm iterating through the $sections array and displaying only those sections which are not attachments and are of type "text/plain". Note the huge array of string functions I'm using to sanitize the body prior to displaying it.

The last section of the script deals with attachments. In this case, my task is even simpler, thanks to the get_attachments() function - all I need to do is iterate through the array returned by get_attachments() and display the file names and sizes in a list.

<?php
// if attachments exist
if (is_array($attachments))
{
?>
<tr>
<td valign=top><font face="Verdana" size="-1"><b>Attachments: </b></font></td>
<td valign=top><font face="Verdana" size="-1"><ul>
<?php
    // display as list
    for($x=0; $x<sizeof($attachments); $x++)
    {
        echo "<li><a href=download.php?id=$id&pid=" . $attachments[$x]["pid"] . ">" . $attachments[$x]["name"] . " (" . ceil($attachments[$x]["size"]/1024) . " KB)</a>";
    }

?>
</ul></font></td>
</tr>
<?php
}
?>

Each attachment can be downloaded separately, by clicking on the link to "download.php". I'll be addressing this shortly - for the moment, note merely that "download.php" gets the message number ($id) and part number ($pid) as GET parameters.

This script needs to be linked to a bunch of other scripts as well. If you look at the command buttons at the top of the generated page, you'll see links to compose a new message and forward, delete or reply to the current message. Some of these scripts are also passed parameters via the GET method - take a brief look at them now and I'll come back to them later.

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

Note how I'm reusing "delete.php" to delete just one message, rather than a list, by passing it data in the format it expects. I *could* write a different version of "delete.php" just to delete a specific message...but I'm lazy, and this works just fine.

Finally - and this was actually an addition I made after a user interface review - it would be nice to have a way to move to the next or previous message from this screen itself, rather than going back to the list and selecting another message. And so, I've added a little code to the bottom of the page to display links to the previous and next message - they merely call "view.php" with a different message number.

<?php if ($id > 1) { // display previous message link echo "<a href=view.php?id=" . ($id-1) .">``<font face=Verdana size=-2>Previous message"; } ?>

<?php
// display next message link
// now you know why I needed $total
if ($id < $total)
{
    echo "`<a href=view.php?id=" . ($id+1) .">``<font face=Verdana size=-2>`Next message</font></a>";
}

?>


Here's what the finished product looks like:

<img src="../../../data/archives/trog/collateral/00102/image2.jpg" />

## Getting Down

You've already seen how every attachment is linked to the script "download.php" via a message and part number. This "download.php" script is fairly interesting - its job is to decode the selected attachment from its plaintext form back into binary data and then allow the user to download it to the local workstation.

<?php

// download.php - download attachment

// includes and session check

// check for required values if (!$id || !$pid) { header("Location: error.php?ec=4"); exit; }

// form not submitted if(!$submit) { ?> <html> <head> <body bgcolor="White">

<?php // page header ?> <font face="Verdana" size="-1"> <form action="<?php echo $PHP_SELF?>" method="post"> <input type="hidden" name="id" value="<?php echo $id; ?>"> <input type="hidden" name="pid" value="<?php echo $pid; ?>"> <input type="submit" name="submit" value="Click here">``<font face="Verdana" size="-1"> to begin downloading the selected attachment to your local workstation. <font face="Verdana" size="-1">Once the attachment has completed downloading, you may <a href="view.php?id=<?php echo $id; ?>">return to the selected message or <a href="list.php">go back to the message listing. <?php } else { // open POP connection $inbox = @imap_open ("{". $SESSION_MAIL_HOST . "/pop3:110}", $SESSION_USER_NAME, $SESSION_USER_PASS) or header("Location: error.php?ec=3");

// parse message structure
$structure = imap_fetchstructure($inbox, $id);
$sections = parse($structure);

// look for specified part
for($x=0; $x<sizeof($sections); $x++)
{
    if($sections[$x]["pid"] == $pid)
    {
    $type = $sections[$x]["type"];
    $encoding = $sections[$x]["encoding"];
    $filename = $sections[$x]["name"];
    }
}
$attachment = imap_fetchbody($inbox, $id, $pid);

// send headers to browser to initiate file download
header ("Content-Type: $type");
header ("Content-Disposition: attachment; filename=$filename");
if ($encoding == "base64")
{
    // decode and send
    echo imap_base64($attachment);
}
else
{
    // add handlers for other encodings here
    echo $attachment;
}

// clean up
imap_close($inbox);

} ?>



After a few basic error checks, the script produces some simple instructions - click a button to initiate file download, or click a link to go back to the main message listing. In this case, the button is actually a form (more on this later), which, once submitted, connects to the POP3 server, selects the specified message, retrieves the message part specified by $pid, decodes it using PHP's built-in BASE64 decoder, and sends HTTP headers to the browser to prepare it for a file download (whew!). Once the browser receives the headers, it should pop up a "Save As" dialog box, allowing the user to save the file to his or her local workstation, where it can be modified and edited.

Note that the filename sent in the "Content-Disposition: " header is the original name of the file.

I'll draw your attention here to the very cool imap_fetchbody() function (not to be confused with imap_body(), which you saw in the previous script), which can be used to retrieve a specific section of a message body, given the part number. This section, once retrieved, usually consists of what looks like gibberish, but is actually text-encoded binary data; it needs to be converted back into its binary representation using an appropriate decoding mechanism. The script above only knows how to handle BASE64 encoding - feel free to add other support for other encoding methods also.

You'll notice also that I've used a form to call the script which actually initiates the download. My original stab at this was to simply call the script and pass it the message and part IDs via the URL GET method - for example, "download.php?id=13&pid=4". However, while this technique worked without a problem in Netscape and lynx browsers, and even in version 5.0 of Internet Explorer, I noticed a problem with Internet Explorer 5.5; the browser chokes if asked to download a script containing GET-type parameters. Consequently, I decided to use a form and pass parameters via the POST method instead.

Some users have also reported another strange problem with Internet Explorer 5.5 - rather than downloading the target file, the browser has a nasty tendency to download the calling script instead. I plan to look into this at some point - if you have any ideas on what this is all about, let me know!

Here's what it looks like:

<img src="../../../data/archives/trog/collateral/00102/image3.jpg" />

## Miles To Go

At this point, I'm fairly pleased with myself. I've succeeded in writing a fully-functional POP3 mail reader, one which is capable of connecting to any POP3 server, retrieving messages from it, and handling MIME-compliant attachments gracefully. Sure, it's not perfect - I've noticed problems with certain types of messages - but those are easily handled by appropriate additions to the parse() function, or by additional checks in "view.php", and you may even decide to ignore them if you're happy with the way the program currently works.

Hopefully, you've also learned a lot more about PHP's IMAP functions. This article introduced you to a few more important pieces of the puzzle, demonstrating how PHP's IMAP functions allow you to obtain detailed structural information on an email message, and extract individual segments from it for further processing. It also provided a crash course on MIME, using examples and theory to set the base for a practical implementation of MIME-based attachment handling.

We're now a lot closer to the end of this case study - all that's left is to write code to handle the four additional functions of compose, reply, forward and send. Come back for the final segment of this case study, and find out more about how I plan to handle these functions, together with a look at the process of constructing a MIME message and a brief discussion of the error handler used throughout this article.

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 on 14 Nov 2001.