├── .gitignore ├── CHANGELOG.md ├── README.md ├── composer.json └── lib └── MimeMailParser ├── Attachment.php ├── Exception └── RuntimeException.php └── Parser.php /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ####v1.0.7 2 | 3 | [FIX] getMessageBody does not handle Content-disposition inline [#11](https://github.com/message/php-mime-mail-parser/pull/11) 4 | 5 | ####v1.0.6 6 | 7 | [FIX] try content-name if disposition-filename isn't set [#6](https://github.com/message/php-mime-mail-parser/pull/6) 8 | 9 | ####v1.0.5 10 | 11 | [ENHANCEMENT] Added `Attachment` usage example to documentation [#5](https://github.com/message/php-mime-mail-parser/pull/5) 12 | 13 | ####v1.0.4 14 | 15 | [FIX] Some private methods changed to protected. Fixed attechment stream 2028 characters issue [#4](https://github.com/message/php-mime-mail-parser/pull/4) 16 | 17 | ####v1.0.3 18 | 19 | [FIX] Added parts variable. Variable was not declared which can lead to annoying error messages in logs [#3](https://github.com/message/php-mime-mail-parser/pull/3) 20 | 21 | [FIX] Removed unused pivate `getPartHeader()` method 22 | 23 | ####v1.0.2 24 | 25 | [FEATURE] Added required extensions to composer [#2](https://github.com/message/php-mime-mail-parser/pull/2) 26 | 27 | ####v1.0.1 28 | 29 | [FIX] Corrects an issue with getting the 'text' body in some instances [#1](https://github.com/message/php-mime-mail-parser/pull/1) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | php-mime-mail-parser 2 | ==================== 3 | 4 | # THIS REPOSITORY IS DEPRECATED. MOVED TO https://github.com/php-mime-mail-parser/php-mime-mail-parser 5 | 6 | PHP 5.3+ Fork of http://code.google.com/p/php-mime-mail-parser 7 | 8 | ## Contributions 9 | Feel free to contribute. 10 | 11 | ## Composer 12 | ```json 13 | "require": { 14 | "messaged/php-mime-mail-parser": "v1.0.7" 15 | } 16 | ``` 17 | 18 | ## Usage example 19 | 20 | ```php 21 | use MimeMailParser\Parser; 22 | use MimeMailParser\Attachment; 23 | 24 | $parser = new Parser(); 25 | $parser->setText(file_get_contents('/path/to/mail')); 26 | 27 | $to = $parser->getHeader('to'); 28 | $delivered_to = $parser->getHeader('delivered-to'); 29 | $from = $parser->getHeader('from'); 30 | $subject = $parser->getHeader('subject'); 31 | $text = $parser->getMessageBody('text'); 32 | $html = $parser->getMessageBody('html'); 33 | $attachments = $parser->getAttachments(); 34 | 35 | // Write attachments to disk 36 | foreach ($attachments as $attachment) { 37 | $attachment->saveAttachment('/tmp'); 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "messaged/php-mime-mail-parser", 3 | "description": "Mailparse extension wrapper for PHP 5.3+", 4 | "keywords": ["mailparse", "mime", "mail", "MimeMailParser"], 5 | "homepage": "http://github.com/message/php-mime-mail-parser", 6 | "type": "library", 7 | "license": "CC BY-SA 3.0", 8 | "authors": [ 9 | { 10 | "name": "Gabe", 11 | "email": "gabe@fijiwebdesign.com", 12 | "homepage": "http://www.fijiwebdesign.com/" 13 | }, 14 | { 15 | "name": "Dmitry Polovka", 16 | "email": "messaged@messaged.lv", 17 | "homepage": "http://messaged.lv" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=5.3", 22 | "ext-mbstring": "*", 23 | "ext-mailparse": "*" 24 | }, 25 | "autoload": { 26 | "psr-0": { 27 | "MimeMailParser": "lib/" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /lib/MimeMailParser/Attachment.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 45 | $this->content_type = $content_type; 46 | $this->stream = $stream; 47 | $this->content = null; 48 | $this->content_disposition = $content_disposition; 49 | $this->headers = $headers; 50 | } 51 | 52 | /** 53 | * retrieve the attachment filename 54 | * @return String 55 | */ 56 | public function getFilename() 57 | { 58 | return $this->filename; 59 | } 60 | 61 | /** 62 | * Retrieve the Attachment Content-Type 63 | * @return String 64 | */ 65 | public function getContentType() 66 | { 67 | return $this->content_type; 68 | } 69 | 70 | /** 71 | * Retrieve the Attachment Content-Disposition 72 | * @return String 73 | */ 74 | public function getContentDisposition() 75 | { 76 | return $this->content_disposition; 77 | } 78 | 79 | /** 80 | * Retrieve the Attachment Headers 81 | * @return String 82 | */ 83 | public function getHeaders() 84 | { 85 | return $this->headers; 86 | } 87 | 88 | /** 89 | * Retrieve the file extension 90 | * @return String 91 | */ 92 | public function getFileExtension() 93 | { 94 | if ( ! $this->extension) { 95 | $ext = substr(strrchr($this->filename, '.'), 1); 96 | if ($ext == 'gz') { 97 | // special case, tar.gz 98 | // todo: other special cases? 99 | $ext = preg_match("/\.tar\.gz$/i", $ext) ? 'tar.gz' : 'gz'; 100 | } 101 | $this->extension = $ext; 102 | } 103 | return $this->extension; 104 | } 105 | 106 | /** 107 | * Read the contents a few bytes at a time until completed 108 | * Once read to completion, it always returns false 109 | * @return String 110 | * @param $bytes Int[optional] 111 | */ 112 | public function read($bytes = 2082) 113 | { 114 | return feof($this->stream) ? false : fread($this->stream, $bytes); 115 | } 116 | 117 | /** 118 | * Retrieve the file content in one go 119 | * Once you retreive the content you cannot use \MimeMailParser\Attachment::read() 120 | * @return String 121 | */ 122 | public function getContent() 123 | { 124 | if ($this->content === null) { 125 | fseek($this->stream, 0); 126 | while (($buf = $this->read()) !== false) { 127 | $this->content .= $buf; 128 | } 129 | } 130 | return $this->content; 131 | } 132 | 133 | /** 134 | * Allow the properties 135 | * \MimeMailParser\Attachment::$name, 136 | * \MimeMailParser\Attachment::$extension 137 | * to be retrieved as public properties 138 | * @param $name Object 139 | */ 140 | public function __get($name) 141 | { 142 | if ($name == 'content') { 143 | return $this->getContent(); 144 | } else if ($name == 'extension') { 145 | return $this->getFileExtension(); 146 | } 147 | 148 | return null; 149 | } 150 | 151 | /** 152 | * Writes attachment into system 153 | * 154 | * @param string $path 155 | * @param string $filename 156 | */ 157 | public function saveAttachment($path, $filename = null) 158 | { 159 | if ( ! is_dir($path)) { 160 | if ( ! mkdir($path)) { 161 | throw new Exception\RuntimeException("Failed to create directory {$path}"); 162 | } 163 | } 164 | 165 | if (is_null($filename)) { 166 | $filename = $this->getFilename(); 167 | } 168 | 169 | $path = rtrim($path, '/') . '/' . $filename; 170 | 171 | return file_put_contents($path, $this->getContent()); 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /lib/MimeMailParser/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | attachment_streams = array(); 50 | } 51 | 52 | /** 53 | * Free the held resouces 54 | * @return void 55 | */ 56 | public function __destruct() 57 | { 58 | // clear the email file resource 59 | if (is_resource($this->stream)) { 60 | fclose($this->stream); 61 | } 62 | // clear the MailParse resource 63 | if (is_resource($this->resource)) { 64 | mailparse_msg_free($this->resource); 65 | } 66 | // remove attachment resources 67 | foreach ($this->attachment_streams as $stream) { 68 | fclose($stream); 69 | } 70 | } 71 | 72 | /** 73 | * Set the file path we use to get the email text 74 | * @return Object MimeMailParser Instance 75 | * @param $path Object 76 | */ 77 | public function setPath($path) 78 | { 79 | // should parse message incrementally from file 80 | $this->resource = mailparse_msg_parse_file($path); 81 | $this->stream = fopen($path, 'r'); 82 | $this->parse(); 83 | return $this; 84 | } 85 | 86 | /** 87 | * Set the Stream resource we use to get the email text 88 | * @return Object MimeMailParser Instance 89 | * @param $stream Resource 90 | */ 91 | public function setStream($stream) 92 | { 93 | 94 | // streams have to be cached to file first 95 | if (get_resource_type($stream) == 'stream') { 96 | $tmp_fp = tmpfile(); 97 | if ($tmp_fp) { 98 | while ( ! feof($stream)) { 99 | fwrite($tmp_fp, fread($stream, 2028)); 100 | } 101 | fseek($tmp_fp, 0); 102 | $this->stream = & $tmp_fp; 103 | } else { 104 | throw new RuntimeException('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'); 105 | } 106 | fclose($stream); 107 | } else { 108 | $this->stream = $stream; 109 | } 110 | 111 | $this->resource = mailparse_msg_create(); 112 | // parses the message incrementally low memory usage but slower 113 | while ( ! feof($this->stream)) { 114 | mailparse_msg_parse($this->resource, fread($this->stream, 2082)); 115 | } 116 | $this->parse(); 117 | return $this; 118 | } 119 | 120 | /** 121 | * Set the email text 122 | * @return Object MimeMailParser Instance 123 | * @param $data String 124 | */ 125 | public function setText($data) 126 | { 127 | $this->resource = mailparse_msg_create(); 128 | // does not parse incrementally, fast memory hog might explode 129 | mailparse_msg_parse($this->resource, $data); 130 | $this->data = $data; 131 | $this->parse(); 132 | return $this; 133 | } 134 | 135 | /** 136 | * Parse the Message into parts 137 | * @return void 138 | * @private 139 | */ 140 | private function parse() 141 | { 142 | $structure = mailparse_msg_get_structure($this->resource); 143 | $this->parts = array(); 144 | foreach ($structure as $part_id) { 145 | $part = mailparse_msg_get_part($this->resource, $part_id); 146 | $this->parts[$part_id] = mailparse_msg_get_part_data($part); 147 | } 148 | } 149 | 150 | /** 151 | * Retrieve the Email Headers 152 | * @return Array 153 | */ 154 | public function getHeaders() 155 | { 156 | if (isset($this->parts[1])) { 157 | return $this->getPartHeaders($this->parts[1]); 158 | } else { 159 | throw new RuntimeException('Parser::setPath() or Parser::setText() must be called before retrieving email headers.'); 160 | } 161 | return false; 162 | } 163 | 164 | /** 165 | * Retrieve the raw Email Headers 166 | * @return string 167 | */ 168 | public function getHeadersRaw() 169 | { 170 | if (isset($this->parts[1])) { 171 | return $this->getPartHeaderRaw($this->parts[1]); 172 | } else { 173 | throw new RuntimeException('Parser::setPath() or Parser::setText() must be called before retrieving email headers.'); 174 | } 175 | return false; 176 | } 177 | 178 | /** 179 | * Retrieve a specific Email Header 180 | * @return String 181 | * @param $name String Header name 182 | */ 183 | public function getHeader($name) 184 | { 185 | if (isset($this->parts[1])) { 186 | $headers = $this->getPartHeaders($this->parts[1]); 187 | if (isset($headers[$name])) { 188 | return $headers[$name]; 189 | } 190 | } else { 191 | throw new RuntimeException('Parser::setPath() or Parser::setText() must be called before retrieving email headers.'); 192 | } 193 | return false; 194 | } 195 | 196 | /** 197 | * Returns the email message body in the specified format 198 | * @return Mixed String Body or False if not found 199 | * @param $type Object[optional] 200 | */ 201 | public function getMessageBody($type = 'text') 202 | { 203 | $body = false; 204 | $mime_types = array( 205 | 'text' => 'text/plain', 206 | 'html' => 'text/html' 207 | ); 208 | if (in_array($type, array_keys($mime_types))) { 209 | foreach ($this->parts as $part) { 210 | if ($this->getPartContentType($part) == $mime_types[$type] && (!$this->getPartContentDisposition($part) || $this->getPartContentDisposition($part) === 'inline')) { 211 | $headers = $this->getPartHeaders($part); 212 | $body = $this->decode($this->getPartBody($part), array_key_exists('content-transfer-encoding', $headers) ? $headers['content-transfer-encoding'] : ''); 213 | } 214 | } 215 | } else { 216 | throw new RuntimeException('Invalid type specified for Parser::getMessageBody. "type" can either be text or html.'); 217 | } 218 | return $body; 219 | } 220 | 221 | /** 222 | * get the headers for the message body part. 223 | * @return Array 224 | * @param $type Object[optional] 225 | */ 226 | public function getMessageBodyHeaders($type = 'text') 227 | { 228 | $headers = false; 229 | $mime_types = array( 230 | 'text' => 'text/plain', 231 | 'html' => 'text/html' 232 | ); 233 | if (in_array($type, array_keys($mime_types))) { 234 | foreach ($this->parts as $part) { 235 | if ($this->getPartContentType($part) == $mime_types[$type]) { 236 | $headers = $this->getPartHeaders($part); 237 | } 238 | } 239 | } else { 240 | throw new RuntimeException('Invalid type specified for Parser::getMessageBody. "type" can either be text or html.'); 241 | } 242 | return $headers; 243 | } 244 | 245 | /** 246 | * Returns the attachments contents in order of appearance 247 | * @return Array 248 | * @param $type Object[optional] 249 | */ 250 | public function getAttachments() 251 | { 252 | $attachments = array(); 253 | $dispositions = array("attachment", "inline"); 254 | foreach ($this->parts as $part) { 255 | $disposition = $this->getPartContentDisposition($part); 256 | if (in_array($disposition, $dispositions)) { 257 | $attachments[] = new Attachment( 258 | !empty($part['disposition-filename']) ? $part['disposition-filename'] : $part['content-name'], 259 | $this->getPartContentType($part), 260 | $this->getAttachmentStream($part), 261 | $disposition, 262 | $this->getPartHeaders($part) 263 | ); 264 | } 265 | } 266 | return $attachments; 267 | } 268 | 269 | /** 270 | * Return the Headers for a MIME part 271 | * @return Array 272 | * @param $part Array 273 | */ 274 | protected function getPartHeaders($part) 275 | { 276 | if (isset($part['headers'])) { 277 | return $part['headers']; 278 | } 279 | return false; 280 | } 281 | 282 | /** 283 | * Return the ContentType of the MIME part 284 | * @return String 285 | * @param $part Array 286 | */ 287 | protected function getPartContentType($part) 288 | { 289 | if (isset($part['content-type'])) { 290 | return $part['content-type']; 291 | } 292 | return false; 293 | } 294 | 295 | /** 296 | * Return the Content Disposition 297 | * @return String 298 | * @param $part Array 299 | */ 300 | protected function getPartContentDisposition($part) 301 | { 302 | if (isset($part['content-disposition'])) { 303 | return $part['content-disposition']; 304 | } 305 | return false; 306 | } 307 | 308 | /** 309 | * Retrieve the raw Header of a MIME part 310 | * @return String 311 | * @param $part Object 312 | */ 313 | protected function getPartHeaderRaw(&$part) 314 | { 315 | $header = ''; 316 | if ($this->stream) { 317 | $header = $this->getPartHeaderFromFile($part); 318 | } else if ($this->data) { 319 | $header = $this->getPartHeaderFromText($part); 320 | } else { 321 | throw new RuntimeException('Parser::setPath() or Parser::setText() must be called before retrieving email parts.'); 322 | } 323 | return $header; 324 | } 325 | 326 | /** 327 | * Retrieve the Body of a MIME part 328 | * @return String 329 | * @param $part Object 330 | */ 331 | protected function getPartBody(&$part) 332 | { 333 | $body = ''; 334 | if ($this->stream) { 335 | $body = $this->getPartBodyFromFile($part); 336 | } else if ($this->data) { 337 | $body = $this->getPartBodyFromText($part); 338 | } else { 339 | throw new RuntimeException('Parser::setPath() or Parser::setText() must be called before retrieving email parts.'); 340 | } 341 | return $body; 342 | } 343 | 344 | /** 345 | * Retrieve the Header from a MIME part from file 346 | * @return String Mime Header Part 347 | * @param $part Array 348 | */ 349 | protected function getPartHeaderFromFile(&$part) 350 | { 351 | $start = $part['starting-pos']; 352 | $end = $part['starting-pos-body']; 353 | fseek($this->stream, $start, SEEK_SET); 354 | $header = fread($this->stream, $end - $start); 355 | return $header; 356 | } 357 | 358 | /** 359 | * Retrieve the Body from a MIME part from file 360 | * @return String Mime Body Part 361 | * @param $part Array 362 | */ 363 | protected function getPartBodyFromFile(&$part) 364 | { 365 | $start = $part['starting-pos-body']; 366 | $end = $part['ending-pos-body']; 367 | fseek($this->stream, $start, SEEK_SET); 368 | $body = fread($this->stream, $end - $start); 369 | return $body; 370 | } 371 | 372 | /** 373 | * Retrieve the Header from a MIME part from text 374 | * @return String Mime Header Part 375 | * @param $part Array 376 | */ 377 | protected function getPartHeaderFromText(&$part) 378 | { 379 | $start = $part['starting-pos']; 380 | $end = $part['starting-pos-body']; 381 | $header = substr($this->data, $start, $end - $start); 382 | return $header; 383 | } 384 | 385 | /** 386 | * Retrieve the Body from a MIME part from text 387 | * @return String Mime Body Part 388 | * @param $part Array 389 | */ 390 | protected function getPartBodyFromText(&$part) 391 | { 392 | $start = $part['starting-pos-body']; 393 | $end = $part['ending-pos-body']; 394 | $body = substr($this->data, $start, $end - $start); 395 | return $body; 396 | } 397 | 398 | /** 399 | * Read the attachment Body and save temporary file resource 400 | * @return String Mime Body Part 401 | * @param $part Array 402 | */ 403 | protected function getAttachmentStream(&$part) 404 | { 405 | $temp_fp = tmpfile(); 406 | 407 | array_key_exists('content-transfer-encoding', $part['headers']) ? $encoding = $part['headers']['content-transfer-encoding'] : $encoding = ''; 408 | 409 | if ($temp_fp) { 410 | if ($this->stream) { 411 | $start = $part['starting-pos-body']; 412 | $end = $part['ending-pos-body']; 413 | fseek($this->stream, $start, SEEK_SET); 414 | $len = $end - $start; 415 | $written = 0; 416 | $write = 2028; 417 | while ($written < $len) { 418 | if (($written + $write < $len)) { 419 | $write = $len - $written; 420 | } else if ($len < $write) { 421 | $write = $len; 422 | } 423 | $part = fread($this->stream, $write); 424 | fwrite($temp_fp, $this->decode($part, $encoding)); 425 | $written += $write; 426 | } 427 | } else if ($this->data) { 428 | $attachment = $this->decode($this->getPartBodyFromText($part), $encoding); 429 | fwrite($temp_fp, $attachment, strlen($attachment)); 430 | } 431 | fseek($temp_fp, 0, SEEK_SET); 432 | } else { 433 | throw new RuntimeException('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'); 434 | } 435 | return $temp_fp; 436 | } 437 | 438 | /** 439 | * Decode the string depending on encoding type. 440 | * @return String the decoded string. 441 | * @param $encodedString The string in its original encoded state. 442 | * @param $encodingType The encoding type from the Content-Transfer-Encoding header of the part. 443 | */ 444 | private function decode($encodedString, $encodingType) 445 | { 446 | if (strtolower($encodingType) == 'base64') { 447 | return base64_decode($encodedString); 448 | } else if (strtolower($encodingType) == 'quoted-printable') { 449 | return quoted_printable_decode($encodedString); 450 | } else { 451 | return $encodedString; 452 | } 453 | } 454 | 455 | } 456 | --------------------------------------------------------------------------------