3 require_once('attachment.class.php');
6 * Fast Mime Mail parser Class using PHP's MailParse Extension
7 * @author gabe@fijiwebdesign.com
8 * @url http://www.fijiwebdesign.com/
9 * @license http://creativecommons.org/licenses/by-sa/3.0/us/
12 class MimeMailParser
{
15 * PHP MimeParser Resource ID
20 * A file pointer to email
30 * Stream Resources for Attachments
32 public $attachment_streams;
38 public function __construct() {
39 $this->attachment_streams
= array();
43 * Free the held resouces
46 public function __destruct() {
47 // clear the email file resource
48 if (is_resource($this->stream
)) {
49 fclose($this->stream
);
51 // clear the MailParse resource
52 if (is_resource($this->resource)) {
53 mailparse_msg_free($this->resource);
55 // remove attachment resources
56 foreach($this->attachment_streams
as $stream) {
62 * Set the file path we use to get the email text
63 * @return Object MimeMailParser Instance
64 * @param $mail_path Object
66 public function setPath($path) {
67 // should parse message incrementally from file
68 $this->resource = mailparse_msg_parse_file($path);
69 $this->stream
= fopen($path, 'r');
75 * Set the Stream resource we use to get the email text
76 * @return Object MimeMailParser Instance
77 * @param $stream Resource
79 public function setStream($stream) {
81 // streams have to be cached to file first
82 if (get_resource_type($stream) == 'stream') {
85 while(!feof($stream)) {
86 fwrite($tmp_fp, fread($stream, 2028));
89 $this->stream
=& $tmp_fp;
91 throw new Exception('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.');
96 $this->stream
= $stream;
99 $this->resource = mailparse_msg_create();
100 // parses the message incrementally low memory usage but slower
101 while(!feof($this->stream
)) {
102 mailparse_msg_parse($this->resource, fread($this->stream
, 2082));
110 * @return Object MimeMailParser Instance
111 * @param $data String
113 public function setText($data) {
114 // NOTE: This has been modified for Phabricator. If the input data does not
115 // end in a newline, Mailparse fails to include the last line in the mail
116 // body. This happens somewhere deep, deep inside the mailparse extension,
117 // so adding a newline here seems like the most straightforward fix.
118 if (!preg_match('/\n\z/', $data)) {
122 $this->resource = mailparse_msg_create();
123 // does not parse incrementally, fast memory hog might explode
124 mailparse_msg_parse($this->resource, $data);
131 * Parse the Message into parts
135 private function parse() {
136 $structure = mailparse_msg_get_structure($this->resource);
137 $this->parts
= array();
138 foreach($structure as $part_id) {
139 $part = mailparse_msg_get_part($this->resource, $part_id);
140 $this->parts
[$part_id] = mailparse_msg_get_part_data($part);
145 * Retrieve the Email Headers
148 public function getHeaders() {
149 if (isset($this->parts
[1])) {
150 return $this->getPartHeaders($this->parts
[1]);
152 throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.');
157 * Retrieve the raw Email Headers
160 public function getHeadersRaw() {
161 if (isset($this->parts
[1])) {
162 return $this->getPartHeaderRaw($this->parts
[1]);
164 throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.');
170 * Retrieve a specific Email Header
172 * @param $name String Header name
174 public function getHeader($name) {
175 if (isset($this->parts
[1])) {
176 $headers = $this->getPartHeaders($this->parts
[1]);
177 if (isset($headers[$name])) {
178 return $headers[$name];
181 throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.');
187 * Returns the email message body in the specified format
188 * @return Mixed String Body or False if not found
189 * @param $type Object[optional]
191 public function getMessageBody($type = 'text') {
193 // NOTE: This function has been modified for Phabricator. The default
194 // implementation returns the last matching part, which throws away text
195 // for many emails. Instead, we concatenate all matching parts. See
196 // issue 22 for discussion:
197 // http://code.google.com/p/php-mime-mail-parser/issues/detail?id=22
201 'text'=> 'text/plain',
204 if (in_array($type, array_keys($mime_types))) {
205 foreach($this->parts
as $part) {
206 $disposition = $this->getPartContentDisposition($part);
207 if ($disposition == 'attachment') {
208 // text/plain parts with "Content-Disposition: attachment" are
209 // attachments, not part of the text body.
212 if ($this->getPartContentType($part) == $mime_types[$type]) {
213 $headers = $this->getPartHeaders($part);
214 // Concatenate all the matching parts into the body text. For example,
215 // if a user sends a message with some text, then an image, and then
216 // some more text, the text body of the email gets split over several
218 $body .= $this->decode(
219 $this->getPartBody($part),
220 array_key_exists('content-transfer-encoding', $headers)
221 ?
$headers['content-transfer-encoding']
226 throw new Exception('Invalid type specified for MimeMailParser::getMessageBody. "type" can either be text or html.');
232 * get the headers for the message body part.
234 * @param $type Object[optional]
236 public function getMessageBodyHeaders($type = 'text') {
239 'text'=> 'text/plain',
242 if (in_array($type, array_keys($mime_types))) {
243 foreach($this->parts
as $part) {
244 if ($this->getPartContentType($part) == $mime_types[$type]) {
245 $headers = $this->getPartHeaders($part);
249 throw new Exception('Invalid type specified for MimeMailParser::getMessageBody. "type" can either be text or html.');
255 * Returns the attachments contents in order of appearance
257 * @param $type Object[optional]
259 public function getAttachments() {
260 // NOTE: This has been modified for Phabricator. Some mail clients do not
261 // send attachments with "Content-Disposition" headers.
262 $attachments = array();
263 $dispositions = array("attachment","inline");
264 $non_attachment_types = array("text/plain", "text/html");
266 foreach ($this->parts
as $part) {
267 $disposition = $this->getPartContentDisposition($part);
268 $filename = 'noname';
269 if (isset($part['disposition-filename'])) {
270 $filename = $part['disposition-filename'];
271 } elseif (isset($part['content-name'])) {
272 // if we have no disposition but we have a content-name, it's a valid attachment.
273 // we simulate the presence of an attachment disposition with a disposition filename
274 $filename = $part['content-name'];
275 $disposition = 'attachment';
276 } elseif (!in_array($part['content-type'], $non_attachment_types, true)
277 && substr($part['content-type'], 0, 10) !== 'multipart/'
279 // if we cannot get it with getMessageBody, we assume it is an attachment
280 $disposition = 'attachment';
283 if (in_array($disposition, $dispositions) && isset($filename) === true) {
284 if ($filename == 'noname') {
286 $filename = 'noname'.$nonameIter;
288 $attachments[] = new MimeMailParser_attachment(
290 $this->getPartContentType($part),
291 $this->getAttachmentStream($part),
293 $this->getPartHeaders($part)
301 * Return the Headers for a MIME part
305 private function getPartHeaders($part) {
306 if (isset($part['headers'])) {
307 return $part['headers'];
313 * Return a Specific Header for a MIME part
316 * @param $header String Header Name
318 private function getPartHeader($part, $header) {
319 if (isset($part['headers'][$header])) {
320 return $part['headers'][$header];
326 * Return the ContentType of the MIME part
330 private function getPartContentType($part) {
331 if (isset($part['content-type'])) {
332 return $part['content-type'];
338 * Return the Content Disposition
342 private function getPartContentDisposition($part) {
343 if (isset($part['content-disposition'])) {
344 return $part['content-disposition'];
350 * Retrieve the raw Header of a MIME part
352 * @param $part Object
354 private function getPartHeaderRaw(&$part) {
357 $header = $this->getPartHeaderFromFile($part);
358 } else if ($this->data
) {
359 $header = $this->getPartHeaderFromText($part);
361 throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.');
366 * Retrieve the Body of a MIME part
368 * @param $part Object
370 private function getPartBody(&$part) {
373 $body = $this->getPartBodyFromFile($part);
374 } else if ($this->data
) {
375 $body = $this->getPartBodyFromText($part);
377 throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.');
383 * Retrieve the Header from a MIME part from file
384 * @return String Mime Header Part
387 private function getPartHeaderFromFile(&$part) {
388 $start = $part['starting-pos'];
389 $end = $part['starting-pos-body'];
390 fseek($this->stream
, $start, SEEK_SET
);
391 $header = fread($this->stream
, $end-$start);
395 * Retrieve the Body from a MIME part from file
396 * @return String Mime Body Part
399 private function getPartBodyFromFile(&$part) {
400 $start = $part['starting-pos-body'];
401 $end = $part['ending-pos-body'];
402 fseek($this->stream
, $start, SEEK_SET
);
403 $body = fread($this->stream
, $end-$start);
408 * Retrieve the Header from a MIME part from text
409 * @return String Mime Header Part
412 private function getPartHeaderFromText(&$part) {
413 $start = $part['starting-pos'];
414 $end = $part['starting-pos-body'];
415 $header = substr($this->data
, $start, $end-$start);
419 * Retrieve the Body from a MIME part from text
420 * @return String Mime Body Part
423 private function getPartBodyFromText(&$part) {
424 $start = $part['starting-pos-body'];
425 $end = $part['ending-pos-body'];
426 $body = substr($this->data
, $start, $end-$start);
431 * Read the attachment Body and save temporary file resource
432 * @return String Mime Body Part
435 private function getAttachmentStream(&$part) {
436 $temp_fp = tmpfile();
438 array_key_exists('content-transfer-encoding', $part['headers']) ?
$encoding = $part['headers']['content-transfer-encoding'] : $encoding = '';
442 $start = $part['starting-pos-body'];
443 $end = $part['ending-pos-body'];
444 fseek($this->stream
, $start, SEEK_SET
);
449 while($written < $len) {
450 if (($written+
$write < $len )) {
451 $write = $len - $written;
453 $part = fread($this->stream
, $write);
454 fwrite($temp_fp, $this->decode($part, $encoding));
457 } else if ($this->data
) {
458 $attachment = $this->decode($this->getPartBodyFromText($part), $encoding);
459 fwrite($temp_fp, $attachment, strlen($attachment));
461 fseek($temp_fp, 0, SEEK_SET
);
463 throw new Exception('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.');
471 * Decode the string depending on encoding type.
472 * @return String the decoded string.
473 * @param $encodedString The string in its original encoded state.
474 * @param $encodingType The encoding type from the Content-Transfer-Encoding header of the part.
476 private function decode($encodedString, $encodingType) {
477 if (strtolower($encodingType) == 'base64') {
478 return base64_decode($encodedString);
479 } else if (strtolower($encodingType) == 'quoted-printable') {
480 return quoted_printable_decode($encodedString);
482 return $encodedString;