Writing a mail client that handles attachments with PHP

When you think about writing a mail client in PHP, beware it's fuzzy because of missing documentation on the imap functions in PHP.
PHP uses the imap functions to fetch and handle mailboxes (also POP3) just a matter of how you connect to your mailaccount.

Making the connection

<?
   $ServerName = "{localhost/imap:143}INBOX"; // For a IMAP connection    (PORT 143)
   $ServerName = "{localhost/pop3:110}INBOX"; // For a POP3 connection    (PORT 110)
   
   $UserName = "YOUR USERNAME";
   $PassWord = "YOUR PASSWORD";
   
   $mbox = imap_open($ServerName, $UserName,$PassWord) or die("Could not open Mailbox - try again later!"); 
?>

Now we've got a connection to the mailbox.

To retrieve some content from a mailbox we now always use the object $mbox which has many features and almost any of these are more or less documented. You can always find information about it on www.php.net search in functions for imap.

Now to an example on how to retrieve a list of all messages:

Message list

<?
   $ServerName = "{localhost/imap:143}INBOX"; // For a IMAP connection    (PORT 143)
   //$ServerName = "{localhost/pop3:110}INBOX"; // For a POP3 connection    (PORT 110)
   
   $UserName = "YOUR USERNAME";
   $PassWord = "YOUR PASSWORD";
   
   $mbox = imap_open($ServerName, $UserName,$PassWord) or die("Could not open Mailbox - try again later!");
   
   if ($hdr = imap_check($mbox)) {
	   echo "Num Messages " . $hdr->Nmsgs ."\n\n<br><br>";
   	$msgCount = $hdr->Nmsgs;
   } else {
   	echo "failed";
   }
   $MN=$msgCount;
   $overview=imap_fetch_overview($mbox,"1:$MN",0);
   $size=sizeof($overview);
   
   echo "<table border=\"0\" cellspacing=\"0\" width=\"582\">";
   
   for($i=$size-1;$i>=0;$i--){
   	$val=$overview[$i];
		$msg=$val->msgno;
   	$from=$val->from;
  		$date=$val->date;
		$subj=$val->subject;
   	$seen=$val->seen;
   
	   $from = ereg_replace("\"","",$from);
   
	   // MAKE DANISH DATE DISPLAY
   	list($dayName,$day,$month,$year,$time) = split(" ",$date); 
		$time = substr($time,0,5);
   	$date = $day ." ". $month ." ". $year . " ". $time;
   
   	if ($bgColor == "#F0F0F0") {
   		$bgColor = "#FFFFFF";
   	} else {
			$bgColor = "#F0F0F0";
		}
   
		if (strlen($subj) > 60) {
   		$subj = substr($subj,0,59) ."...";
		}
   
   	echo "<tr bgcolor=\"$bgColor\"><td colspan=\"2\">$from</td><td colspan=\"2\">$subj</td>
   		 <td class=\"tblContent\" colspan=\"2\">$date</td></tr>\n";
   }
	echo "</table>";
   imap_close($mbox);
?>

This produces a message list where you can read the subject and the date and time for arrival in you mailbox.

The main PHP function used is the imap_fetch_overview($mbox,"1:$MN",0); in this example it tells PHP to fetch message 1 to $MN ($msgCount = the last message from the connection $mbox).

For each message we retrieve in the main "for loop" some properties.
Thats made with:
$val=$overview[$i];
$msg=$val->msgno;
$from=$val->from;
$date=$val->date;
$subj=$val->subject;
$seen=$val->seen;

$msg is getting the message number that identifies a single message from the mailbox. The first one is 1
$from is getting the senders name
$date is getting the date and time of the arrival in your mailbox.
$subj is getting the subject of the message
$seen is either 0 or 1 if 1 the message body has been retrieved earlier.

Thats practically all we need to know for now to produce a list of all the messages.
The seen property is not used in my example but you could implement it easily with an if statement that writes a not seen message (with property of 0) in bold if you want an overview over new messages especially if you are connecting to a IMAP account.

Now we'll take a look on how to retrive the body of the message.

Body retrieval

To retrieve the body of a message I use a nice script posted in www.php.net I thank the author :-)

<?
   function get_mime_type(&$structure) {
   $primary_mime_type = array("TEXT", "MULTIPART","MESSAGE", "APPLICATION", "AUDIO","IMAGE", "VIDEO", "OTHER");
   if($structure->subtype) {
   	return $primary_mime_type[(int) $structure->type] . '/' .$structure->subtype;
   }
   	return "TEXT/PLAIN";
   }
   function get_part($stream, $msg_number, $mime_type, $structure = false,$part_number    = false) {
   
   	if(!$structure) {
   		$structure = imap_fetchstructure($stream, $msg_number);
   	}
   	if($structure) {
   		if($mime_type == get_mime_type($structure)) {
   			if(!$part_number) {
   				$part_number = "1";
   			}
   			$text = imap_fetchbody($stream, $msg_number, $part_number);
   			if($structure->encoding == 3) {
   				return imap_base64($text);
   			} else if($structure->encoding == 4) {
   				return imap_qprint($text);
   			} else {
   			return $text;
   		}
   	}
   
		if($structure->type == 1) /* multipart */ {
   		while(list($index, $sub_structure) = each($structure->parts)) {
   			if($part_number) {
   				$prefix = $part_number . '.';
   			}
   			$data = get_part($stream, $msg_number, $mime_type, $sub_structure,$prefix .    ($index + 1));
   			if($data) {
   				return $data;
   			}
   		} // END OF WHILE
   		} // END OF MULTIPART
   	} // END OF STRUTURE
   	return false;
   } // END OF FUNCTION
   
?> 

The function get_part() needs 3 parameters.
1. Mailbox connection (e.g. $mbox from my connection example)
2. Message number to look up (e.g. $msg from my message list example)
3. A content type to check for

In my following example I'll check for the content types of TEXT/PLAIN and TEXT/HTML which are the common MIME of an email message body. However when it comes to attachments you simply don't know what MIME the attachment has. This gives us a problem we'll deal with in a very short time. - Hang on!

To get the message body we invoke the function get_part() like this:

 <?
   // GET TEXT BODY
   $dataTxt = get_part($mbox, $msgno, "TEXT/PLAIN");
   
   // GET HTML BODY
   $dataHtml = get_part($mbox, $msgno, "TEXT/HTML");
   
   if ($dataHtml != "") {
	   $msgBody = $dataHtml;
   	$mailformat = "html";
   } else {
   	$msgBody = ereg_replace("\n","<br>",$dataTxt);
   	$mailformat = "text";
   }
	// To out put the message body to the user simply print $msgBody like this.
   
   if ($mailformat == "text") {
   	echo "<html><head><title>Messagebody</title></head><body    bgcolor=\"white\">$msgBody</body></html>";
   } else {
   	echo $msgBody; // It contains all HTML HEADER tags so we don't have to make them.
   }
?> 

To make the text-body more userfriendly in our browser we could make it more HTML-like if we would replace all URLs with an actual HYPERLINK on. And sometimes messages in HTML are sent with a character encoding that looks very weird because the HTML is posted raw from some email clients, we'll also change that.

To do that we use this replacement statements:

 function transformHTML($str) {
   if ((strpos($str,"<HTML") < 0) || (strpos($str,"<html")    < 0)) {
  		$makeHeader = "<html><head><meta http-equiv=\"Content-Type\"    content=\"text/html; charset=iso-8859-1\"></head>\n";
   	if ((strpos($str,"<BODY") < 0) || (strpos($str,"<body")    < 0)) {
   		$makeBody = "\n<body>\n";
   		$str = $makeHeader . $makeBody . $str ."\n</body></html>";
   	} else {
   		$str = $makeHeader . $str ."\n</html>";
   	}
   } else {
   	$str = "<meta http-equiv=\"Content-Type\" content=\"text/html;    charset=iso-8859-1\">\n". $str;
   }
   	return $str;
 }
   
 if ($dataHtml != "") {
	$msgBody = transformHTML($dataHtml);
 } else {
   $msgBody = ereg_replace("\n","<br>",$dataTxt);
   $msgBody = preg_replace("/([^\w\/])(www\.[a-z0-9\-]+\.[a-z0-9\-]+)/i","$1http://$2",    $msgBody);
   $msgBody = preg_replace("/([\w]+:\/\/[\w-?&;#~=\.\/\@]+[\w\/])/i","<A    TARGET=\"_blank\" HREF=\"$1\">$1</A>", $msgBody);
   $msgBody = preg_replace("/([\w-?&;#~=\.\/]+\@(\[?)[a-zA-Z0-9\-\.]+\.([a-zA-Z]{2,3}|[0-9]{1,3})(\]?))/i","<A    HREF=\"mailto:$1\">$1</A>",$msgBody);
 }

With this replacement you'll see real hyperlinks in the text-body. And now comes the fuzzy part - attachmenthandling.

Attachment handling

Attachments are parts of a messagebody if the messagebody contains 2 or more parts there should be a file attached to the message. Though Outlook 2000 and Outlook Express have support for "Stationary" - embedded HTML layout with images and so. This is really painful to make visible in the HTML body. I've not made it work YET!

First we'll make a list of the attachments from a message.
In my example it's a FORM SELECT box.

<?
   $struct = imap_fetchstructure($mbox,$msgno);
   $contentParts = count($struct->parts);
   
   if ($contentParts >= 2) {
	   for ($i=2;$i<=$contentParts;$i++) {
   	$att[$i-2] = imap_bodystruct($mbox,$msgno,$i);
   	}
   	for ($k=0;$k<sizeof($att);$k++) {
   		if ($att[$k]->parameters[0]->value == "us-ascii" || $att[$k]->parameters[0]->value    == "US-ASCII") {
   			if ($att[$k]->parameters[1]->value != "") {
   				$selectBoxDisplay[$k] = $att[$k]->parameters[1]->value;
   			}
   		} elseif ($att[$k]->parameters[0]->value != "iso-8859-1" &&    $att[$k]->parameters[0]->value != "ISO-8859-1") {
   			$selectBoxDisplay[$k] = $att[$k]->parameters[0]->value;
   		}
   	}
   }
   
   if (sizeof($selectBoxDisplay) > 0) {
   	echo "<select name=\"attachments\" size=\"3\" class=\"tblContent\"    onChange=\"handleFile(this.value)\" style=\"width:170;\">";
   	for ($j=0;$j<sizeof($selectBoxDisplay);$j++) {
   		echo "\n<option value=\"$j\">". $selectBoxDisplay[$j]    ."</option>";
   	}
   	echo "</select>";
   }
?>

Now we've made a selectbox with the attached files listed.
The thing is now - well it's a beautiful list but we want to by able to download the files. To force download of HTML file instead of have it opened in the browser window we need to use a state of the art browser like Microsoft Internet Explorer 5.5 or later. This browser can make use of a Content-Disposition header which can hold an attachment so the browser will prompt with a Save file dialog box. But as I've seen on www.w3c.org this is going to be a standard. So we can only hope Netscape/Mozilla will support it soon. I don't know if Netscape 6 supports it in some way. I haven't made my example work in any other browser than MSIE 5.5 and 6. Now to the download file stuff.

The selectbox has a onChange eventhandler, if you click on a file in the selectbox the handleFile javascript function is triggered.
Here is the javascript:

<script language="JavaScript">
   var b;
   browser = navigator.appName;
   if (browser == "Microsoft Internet Explorer") {
   	b = "ie";
   } else {
   	b = "other";
   }
   
   function handleFile(nr) {
   	if (b != "ie") {
   		alert("This feature is currently only available for Microsoft Internet Explorer 5.5+ users\n\nWait for an update!");
   	} else {
   		check = confirm("Do you want to download the file ?");
   		if (check) {
   			setTimeout("this.location.reload()",8000);
   			location.href="gotodownload.php?download=1&file="+ nr +"&msgno=<?= $msgno ?>";
   		} else {
   			location.reload();
   		}
   	}
   }
</script>

The script asks you whether you want to download the file. If you click the OK button you'll be redirected to the file gotodownload.php. The redirect has 3 parameters in the querystring: download=1 to force a download, file="+ nr the attached file you clicked (selected) in the selectbox and finally the message number that identifies the message that holds the attached file.

The file gotodownload.php looks like this.

<?
   if ($download == "1") {
   	$strFileName = $att[$file]->parameters[0]->value;
   	$strFileType = strrev(substr(strrev($strFileName),0,4));
   	$fileContent = imap_fetchbody($mbox,$msgno,$file+2);
   	downloadFile($strFileType,$strFileName,$fileContent);
   }
   
   function downloadFile($strFileType,$strFileName,$fileContent) {
   	$ContentType = "application/octet-stream";
   
   	if ($strFileType == ".asf") 
   		$ContentType = "video/x-ms-asf";
   	if ($strFileType == ".avi")
   		$ContentType = "video/avi";
   	if ($strFileType == ".doc")
   		$ContentType = "application/msword";
   	if ($strFileType == ".zip")
   		$ContentType = "application/zip";
   	if ($strFileType == ".xls")
   		$ContentType = "application/vnd.ms-excel";
   	if ($strFileType == ".gif")
   		$ContentType = "image/gif";
   	if ($strFileType == ".jpg" || $strFileType == "jpeg")
   		$ContentType = "image/jpeg";
   	if ($strFileType == ".wav")
   		$ContentType = "audio/wav";
   	if ($strFileType == ".mp3")
   		$ContentType = "audio/mpeg3";
   	if ($strFileType == ".mpg" || $strFileType == "mpeg")
   		$ContentType = "video/mpeg";
   	if ($strFileType == ".rtf")
   		$ContentType = "application/rtf";
   	if ($strFileType == ".htm" || $strFileType == "html")
   		$ContentType = "text/html";
   	if ($strFileType == ".xml") 
   		$ContentType = "text/xml";
   	if ($strFileType == ".xsl") 
   		$ContentType = "text/xsl";
   	if ($strFileType == ".css") 
   		$ContentType = "text/css";
   	if ($strFileType == ".php") 
   		$ContentType = "text/php";
   	if ($strFileType == ".asp") 
   		$ContentType = "text/asp";
   	if ($strFileType == ".pdf")
   		$ContentType = "application/pdf";
   
	header ("Content-Type: $ContentType"); 
	header ("Content-Disposition: attachment; filename=$strFileName; size=$fileSize;"); 
	
	// Updated oktober 29. 2005
	if (substr($ContentType,0,4) == "text") {
	echo imap_qprint($fileContent);
	} else {
	echo imap_base64($fileContent);
	}
    }
?>

You'll see that you're actually not being redirected to the file called gotodownload.php instead you'll be prompted with a Save file dialog if you're using Microsoft Internet Explorer 5.5 or later.

In most cases you can download the file. Sometimes it won't and I'm still on the hunt why!
I have som concerns in the Microsoft Windows/MSIE system making somekind of nothing when you're downloading files associated with programs that are trying to be opened directly from the browser session. What happens if it fails is that you can see the browser is loading something but you're never asked where on your filesystem to store the file.

After all I hope you find my examples useful.
Anyways feel free to question me: php-dev(at)steffer(dot)dk

Thank you for letting me take your time
C ya 'round
Kevin Steffer
(a.k.a.: Echelon, EchelonDK, gutter|echelon, focusman, focusman7, ksteffer, mac_that_dog .... look @ #linuxhelp on Undernet IRC)

I've started a discussion thread on this subject - be helpful - contribute if you want.
Go to discussion

See more articles by Kevin Steffer Overview