splitBodyHeader($message); $this->decode($header, $body); } public function getMainBody() { # Look for the main body part (text/plain or text/html) if ($this->body && $this->ctype_primary == 'text' && $this->ctype_secondary == 'plain') { return $this->body; } foreach ($this->parts as $part) { if (($body = $part->getBody()) !== null) { return $body; } } return; } public function getBody() { return $this->body; } public function getHeaders() { return $this->headers; } public function getHeader($hdr, $only_last = false) { $hdr = strtolower($hdr); if (!isset($this->headers[$hdr])) { return; } if ($only_last && is_array($this->headers[$hdr])) { $r = $this->headers[$hdr]; return array_pop($r); } return $this->headers[$hdr]; } public function getFrom() { return [$this->from_email, $this->from_name]; } public function getAllFiles() { $parts = []; foreach ($this->parts as $part) { $body = $part->getBody(); $filename = $part->getFileName(); if ($body && $filename) { $parts[] = [ 'filename' => $filename, 'content' => $part->getBody() ]; } else { $parts = array_merge($parts, $part->getAllFiles()); } } return $parts; } public function getFileName() { if (isset($this->ctype_parameters['name'])) { return $this->ctype_parameters['name']; } if (isset($this->d_parameters['filename'])) { return $this->d_parameters['filename']; } return; } protected function decode($headers, $body, $default_ctype = 'text/plain') { $headers = $this->parseHeaders($headers); foreach ($headers as $v) { if (isset($this->headers[strtolower($v['name'])]) && !is_array($this->headers[strtolower($v['name'])])) { $this->headers[strtolower($v['name'])] = [$this->headers[strtolower($v['name'])]]; $this->headers[strtolower($v['name'])][] = $v['value']; } elseif (isset($this->headers[strtolower($v['name'])])) { $this->headers[strtolower($v['name'])][] = $v['value']; } else { $this->headers[strtolower($v['name'])] = $v['value']; } } foreach ($headers as $k => $v) { $headers[$k]['name'] = strtolower($headers[$k]['name']); switch ($headers[$k]['name']) { case 'from': list($this->from_name, $this->from_email) = $this->decodeSender($headers[$k]['value']); break; case 'content-type': $content_type = $this->parseHeaderValue($headers[$k]['value']); if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) { $this->ctype_primary = strtolower($regs[1]); $this->ctype_secondary = strtolower($regs[2]); } if (isset($content_type['other'])) { while (list($p_name, $p_value) = each($content_type['other'])) { $this->ctype_parameters[$p_name] = $p_value; } } break; case 'content-disposition': $content_disposition = $this->parseHeaderValue($headers[$k]['value']); $this->disposition = $content_disposition['value']; if (isset($content_disposition['other'])) { while (list($p_name, $p_value) = each($content_disposition['other'])) { $this->d_parameters[$p_name] = $p_value; } } break; case 'content-transfer-encoding': $content_transfer_encoding = $this->parseHeaderValue($headers[$k]['value']); break; } } if (isset($content_type)) { switch (strtolower($content_type['value'])) { case 'text/plain': case 'text/html': $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; $charset = isset($this->ctype_parameters['charset']) ? $this->ctype_parameters['charset'] : null; $this->body = $this->decodeBody($body, $encoding); $this->body = text::cleanUTF8(@text::toUTF8($this->body, $charset)); break; case 'multipart/parallel': case 'multipart/appledouble': // Appledouble mail case 'multipart/report': // RFC1892 case 'multipart/signed': // PGP case 'multipart/digest': case 'multipart/alternative': case 'multipart/related': case 'multipart/mixed': if (!isset($content_type['other']['boundary'])) { throw new Exception('No boundary found'); } $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain'; $parts = $this->boundarySplit($body, $content_type['other']['boundary']); for ($i = 0; $i < count($parts); $i++) { $this->parts[] = new self($parts[$i]); } break; case 'message/rfc822': $this->parts[] = new self($body); break; default: if (!isset($content_transfer_encoding['value'])) { $content_transfer_encoding['value'] = '7bit'; } $this->body = $this->decodeBody($body, $content_transfer_encoding['value']); break; } } else { $ctype = explode('/', $default_ctype); $this->ctype_primary = $ctype[0]; $this->ctype_secondary = $ctype[1]; $this->body = $this->decodeBody($body, '7bit'); $this->body = text::cleanUTF8(@text::toUTF8($this->body)); } } protected function splitBodyHeader($input) { if (preg_match('/^(.*?)\r?\n\r?\n(.*)/s', $input, $match)) { return [$match[1], $match[2]]; } else { # No body found return [$input, '']; } } protected function parseHeaders($input) { if (!$input) { return []; } # Unfold the input $input = preg_replace("/\r?\n/", "\r\n", $input); $input = preg_replace("/\r\n(\t| )+/", ' ', $input); $headers = explode("\r\n", trim($input)); $res = []; # Remove first From line if exists if (strpos($headers[0], 'From ') === 0) { array_shift($headers); } foreach ($headers as $value) { $hdr_name = substr($value, 0, $pos = strpos($value, ':')); $hdr_value = substr($value, $pos + 1); if ($hdr_value[0] == ' ') { $hdr_value = substr($hdr_value, 1); } $res[] = [ 'name' => $hdr_name, 'value' => $this->decodeHeader($hdr_value) ]; } return $res; } protected function parseHeaderValue($input) { if (($pos = strpos($input, ';')) !== false) { $return['value'] = trim(substr($input, 0, $pos)); $input = trim(substr($input, $pos + 1)); if (strlen($input) > 0) { # This splits on a semi-colon, if there's no preceeding backslash # Now works with quoted values; had to glue the \; breaks in PHP # the regex is already bordering on incomprehensible $splitRegex = '/([^;\'"]*[\'"]([^\'"]*([^\'"]*)*)[\'"][^;\'"]*|([^;]+))(;|$)/'; preg_match_all($splitRegex, $input, $matches); $parameters = []; for ($i = 0; $i < count($matches[0]); $i++) { $param = $matches[0][$i]; while (substr($param, -2) == '\;') { $param .= $matches[0][++$i]; } $parameters[] = $param; } for ($i = 0; $i < count($parameters); $i++) { $param_name = trim(substr($parameters[$i], 0, $pos = strpos($parameters[$i], '=')), "'\";\t\\ "); $param_value = trim(str_replace('\;', ';', substr($parameters[$i], $pos + 1)), "'\";\t\\ "); if ($param_value[0] == '"') { $param_value = substr($param_value, 1, -1); } if (preg_match('/\*$/', $param_name)) { $return['other'][strtolower(substr($param_name, 0, -1))] = $this->parserHeaderSpecialValue($param_value); } else { $return['other'][strtolower($param_name)] = $param_value; } } } } else { $return['value'] = trim($input); } return $return; } protected function parserHeaderSpecialValue($value) { if (strpos($value, "''") === false) { return $value; } list($charset, $value) = explode("''", $value); return @text::toUTF8(rawurldecode($value), $charset); } protected function boundarySplit($input, $boundary) { $parts = []; $bs_possible = substr($boundary, 2, -2); $bs_check = '\"' . $bs_possible . '\"'; if ($boundary == $bs_check) { $boundary = $bs_possible; } $tmp = explode('--' . $boundary, $input); for ($i = 1; $i < count($tmp) - 1; $i++) { $parts[] = $tmp[$i]; } return $parts; } protected function decodeHeader($input) { # Remove white space between encoded-words $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input); # Non encoded if (!preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input)) { return @text::toUTF8($input); } # For each encoded-word... while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) { $encoded = $matches[1]; $charset = $matches[2]; $encoding = $matches[3]; $text = $matches[4]; switch (strtolower($encoding)) { case 'b': $text = base64_decode($text); break; case 'q': $text = str_replace('_', ' ', $text); preg_match_all('/=([a-f0-9]{2})/i', $text, $matches); foreach ($matches[1] as $value) { $text = str_replace('=' . $value, chr(hexdec($value)), $text); } break; } $text = @text::toUTF8($text, $charset); $input = str_replace($encoded, $text, $input); } return text::cleanUTF8($input); } protected function decodeSender($sender) { if (preg_match('/([\'|"])?(.*)(?(1)[\'|"])\s+<([\w\-=!#$%^*\'+\\.={}|?~]+@[\w\-=!#$%^*\'+\\.={}|?~]+[\w\-=!#$%^*\'+\\={}|?~])>/', $sender, $matches)) { # Match address in the form: Name $result[0] = $matches[2]; $result[1] = $matches[sizeof($matches) - 1]; } elseif (preg_match('/([\w\-=!#$%^*\'+\\.={}|?~]+@[\w\-=!#$%^*\'+\\.={}|?~]+[\w\-=!#$%^*\'+\\={}|?~])\s+\((.*)\)/', $sender, $matches)) { # Match address in the form: email@host (Name) $result[0] = $matches[1]; $result[1] = $matches[2]; } else { # Only the email address present $result[0] = $sender; $result[1] = $sender; } $result[0] = str_replace("\"", "", $result[0]); $result[0] = str_replace("'", "", $result[0]); return $result; } protected function decodeBody($input, $encoding = '7bit') { switch (strtolower($encoding)) { case '7bit': return $input; break; case 'quoted-printable': return $this->quotedPrintableDecode($input); break; case 'base64': return base64_decode($input); break; default: return $input; } } protected function quotedPrintableDecode($input) { // Remove soft line breaks $input = preg_replace("/=\r?\n/", '', $input); // Replace encoded characters $input = preg_replace_callback('/=([a-f0-9]{2})/i', [$this, 'quotedPrintableDecodeHandler'], $input); return $input; } protected function quotedPrintableDecodeHandler($m) { return chr(hexdec($m[1])); } }