Update Slowvote to use sensible string constants for response visibility
[phabricator.git] / externals / mimemailparser / MimeMailParser.class.php
blob914f50888e7ae181abcfe6d508c5636ab8cbbee2
1 <?php
3 require_once('attachment.class.php');
5 /**
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/
10 * @version $Id$
12 class MimeMailParser {
14 /**
15 * PHP MimeParser Resource ID
17 public $resource;
19 /**
20 * A file pointer to email
22 public $stream;
24 /**
25 * A text of an email
27 public $data;
29 /**
30 * Stream Resources for Attachments
32 public $attachment_streams;
34 /**
35 * Inialize some stuff
36 * @return
38 public function __construct() {
39 $this->attachment_streams = array();
42 /**
43 * Free the held resouces
44 * @return void
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) {
57 fclose($stream);
61 /**
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');
70 $this->parse();
71 return $this;
74 /**
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') {
83 $tmp_fp = tmpfile();
84 if ($tmp_fp) {
85 while(!feof($stream)) {
86 fwrite($tmp_fp, fread($stream, 2028));
88 fseek($tmp_fp, 0);
89 $this->stream =& $tmp_fp;
90 } else {
91 throw new Exception('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.');
92 return false;
94 fclose($stream);
95 } else {
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));
104 $this->parse();
105 return $this;
109 * Set the email text
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)) {
119 $data = $data."\n";
122 $this->resource = mailparse_msg_create();
123 // does not parse incrementally, fast memory hog might explode
124 mailparse_msg_parse($this->resource, $data);
125 $this->data = $data;
126 $this->parse();
127 return $this;
131 * Parse the Message into parts
132 * @return void
133 * @private
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
146 * @return Array
148 public function getHeaders() {
149 if (isset($this->parts[1])) {
150 return $this->getPartHeaders($this->parts[1]);
151 } else {
152 throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.');
154 return false;
157 * Retrieve the raw Email Headers
158 * @return string
160 public function getHeadersRaw() {
161 if (isset($this->parts[1])) {
162 return $this->getPartHeaderRaw($this->parts[1]);
163 } else {
164 throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.');
166 return false;
170 * Retrieve a specific Email Header
171 * @return String
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];
180 } else {
181 throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.');
183 return false;
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
199 $body = false;
200 $mime_types = array(
201 'text'=> 'text/plain',
202 'html'=> 'text/html'
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.
210 continue;
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
217 // attachments.
218 $body .= $this->decode(
219 $this->getPartBody($part),
220 array_key_exists('content-transfer-encoding', $headers)
221 ? $headers['content-transfer-encoding']
222 : '');
225 } else {
226 throw new Exception('Invalid type specified for MimeMailParser::getMessageBody. "type" can either be text or html.');
228 return $body;
232 * get the headers for the message body part.
233 * @return Array
234 * @param $type Object[optional]
236 public function getMessageBodyHeaders($type = 'text') {
237 $headers = false;
238 $mime_types = array(
239 'text'=> 'text/plain',
240 'html'=> 'text/html'
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);
248 } else {
249 throw new Exception('Invalid type specified for MimeMailParser::getMessageBody. "type" can either be text or html.');
251 return $headers;
255 * Returns the attachments contents in order of appearance
256 * @return Array
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");
265 $nonameIter = 0;
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') {
285 $nonameIter++;
286 $filename = 'noname'.$nonameIter;
288 $attachments[] = new MimeMailParser_attachment(
289 $filename,
290 $this->getPartContentType($part),
291 $this->getAttachmentStream($part),
292 $disposition,
293 $this->getPartHeaders($part)
297 return $attachments;
301 * Return the Headers for a MIME part
302 * @return Array
303 * @param $part Array
305 private function getPartHeaders($part) {
306 if (isset($part['headers'])) {
307 return $part['headers'];
309 return false;
313 * Return a Specific Header for a MIME part
314 * @return Array
315 * @param $part Array
316 * @param $header String Header Name
318 private function getPartHeader($part, $header) {
319 if (isset($part['headers'][$header])) {
320 return $part['headers'][$header];
322 return false;
326 * Return the ContentType of the MIME part
327 * @return String
328 * @param $part Array
330 private function getPartContentType($part) {
331 if (isset($part['content-type'])) {
332 return $part['content-type'];
334 return false;
338 * Return the Content Disposition
339 * @return String
340 * @param $part Array
342 private function getPartContentDisposition($part) {
343 if (isset($part['content-disposition'])) {
344 return $part['content-disposition'];
346 return false;
350 * Retrieve the raw Header of a MIME part
351 * @return String
352 * @param $part Object
354 private function getPartHeaderRaw(&$part) {
355 $header = '';
356 if ($this->stream) {
357 $header = $this->getPartHeaderFromFile($part);
358 } else if ($this->data) {
359 $header = $this->getPartHeaderFromText($part);
360 } else {
361 throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.');
363 return $header;
366 * Retrieve the Body of a MIME part
367 * @return String
368 * @param $part Object
370 private function getPartBody(&$part) {
371 $body = '';
372 if ($this->stream) {
373 $body = $this->getPartBodyFromFile($part);
374 } else if ($this->data) {
375 $body = $this->getPartBodyFromText($part);
376 } else {
377 throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.');
379 return $body;
383 * Retrieve the Header from a MIME part from file
384 * @return String Mime Header Part
385 * @param $part Array
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);
392 return $header;
395 * Retrieve the Body from a MIME part from file
396 * @return String Mime Body Part
397 * @param $part Array
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);
404 return $body;
408 * Retrieve the Header from a MIME part from text
409 * @return String Mime Header Part
410 * @param $part Array
412 private function getPartHeaderFromText(&$part) {
413 $start = $part['starting-pos'];
414 $end = $part['starting-pos-body'];
415 $header = substr($this->data, $start, $end-$start);
416 return $header;
419 * Retrieve the Body from a MIME part from text
420 * @return String Mime Body Part
421 * @param $part Array
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);
427 return $body;
431 * Read the attachment Body and save temporary file resource
432 * @return String Mime Body Part
433 * @param $part Array
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 = '';
440 if ($temp_fp) {
441 if ($this->stream) {
442 $start = $part['starting-pos-body'];
443 $end = $part['ending-pos-body'];
444 fseek($this->stream, $start, SEEK_SET);
445 $len = $end-$start;
446 $written = 0;
447 $write = 2028;
448 $body = '';
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));
455 $written += $write;
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);
462 } else {
463 throw new Exception('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.');
464 return false;
466 return $temp_fp;
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);
481 } else {
482 return $encodedString;