├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Decode.php ├── Exception ├── ExceptionInterface.php ├── InvalidArgumentException.php └── RuntimeException.php ├── Message.php ├── Mime.php └── Part.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 2.7.3 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 2.7.2 - 2019-10-16 28 | 29 | ### Added 30 | 31 | - [#37](https://github.com/zendframework/zend-mime/pull/37) adds support for PHP 7.3. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - Nothing. 44 | 45 | ### Fixed 46 | 47 | - [#36](https://github.com/zendframework/zend-mime/pull/36) fixes 48 | `Zend\Mime\Decode::splitMessage` to set `Zend\Mail\Headers` 49 | instance always for `$headers` parameter. Before, when messages 50 | without headers was provided, `$headers` was an empty array. 51 | 52 | ## 2.7.1 - 2018-05-14 53 | 54 | ### Added 55 | 56 | - Nothing. 57 | 58 | ### Changed 59 | 60 | - Nothing. 61 | 62 | ### Deprecated 63 | 64 | - Nothing. 65 | 66 | ### Removed 67 | 68 | - Nothing. 69 | 70 | ### Fixed 71 | 72 | - [#32](https://github.com/zendframework/zend-mime/pull/32) corrects a potential infinite loop when parsing lines consisting of only spaces and dots. 73 | 74 | ## 2.7.0 - 2017-11-28 75 | 76 | ### Added 77 | 78 | - [#27](https://github.com/zendframework/zend-mime/pull/27) adds a fluent 79 | interface to the various setters in `Zend\Mime\Message`. 80 | 81 | - [#28](https://github.com/zendframework/zend-mime/pull/28) adds support for PHP 82 | versions 7.1 and 7.2. 83 | 84 | ### Deprecated 85 | 86 | - Nothing. 87 | 88 | ### Removed 89 | 90 | - [#28](https://github.com/zendframework/zend-mime/pull/28) removes support for 91 | PHP 5.5. 92 | 93 | - [#28](https://github.com/zendframework/zend-mime/pull/28) removes support for 94 | HHVM. 95 | 96 | ### Fixed 97 | 98 | - [#26](https://github.com/zendframework/zend-mime/pull/26) ensures commas 99 | included within list data items are ASCII encoded, ensuring that the items 100 | will split on commas correctly (instead of splitting within an item). 101 | 102 | - [#30](https://github.com/zendframework/zend-mime/pull/30) fixes how EOL 103 | characters are detected, to ensure that mail using `\r\n` as an EOL sequence 104 | (including mail emitted by Cyrus and Dovecot) will be properly parsed. 105 | 106 | ## 2.6.1 - 2017-01-16 107 | 108 | ### Added 109 | 110 | - [#22](https://github.com/zendframework/zend-mime/pull/22) adds the ability to 111 | decode a single-part MIME message via `Zend\Mime\Message::createFromMessage()` 112 | by omitting the `$boundary` argument. 113 | 114 | ### Changes 115 | 116 | - [#14](https://github.com/zendframework/zend-mime/pull/14) adds checks for 117 | duplicate parts when adding them to a MIME message, and now throws an 118 | `InvalidArgumentException` when detected. 119 | 120 | ### Deprecated 121 | 122 | - Nothing. 123 | 124 | ### Removed 125 | 126 | - Nothing. 127 | 128 | ### Fixed 129 | 130 | - [#13](https://github.com/zendframework/zend-mime/pull/13) fixes issues with 131 | qp-octets produced by Outlook. 132 | - [#17](https://github.com/zendframework/zend-mime/pull/17) fixes a syntax error 133 | in how are thrown by `Zend\Mime\Part::setContent()`. 134 | - [#18](https://github.com/zendframework/zend-mime/pull/18) fixes how non-ASCII 135 | header values are encoded, ensuring that it allows the first word to be of 136 | arbitrary length. 137 | 138 | ## 2.6.0 - 2016-04-20 139 | 140 | ### Added 141 | 142 | - [#6](https://github.com/zendframework/zend-mime/pull/6) adds 143 | `Mime::mimeDetectCharset()`, which can be used to detect the charset 144 | of a given string (usually a header) according to the rules specified in 145 | RFC-2047. 146 | 147 | ### Deprecated 148 | 149 | - Nothing. 150 | 151 | ### Removed 152 | 153 | - Nothing. 154 | 155 | ### Fixed 156 | 157 | - Nothing. 158 | 159 | ## 2.5.2 - 2016-04-20 160 | 161 | ### Added 162 | 163 | - [#8](https://github.com/zendframework/zend-mime/pull/8) and 164 | [#11](https://github.com/zendframework/zend-mime/pull/11) port documentation 165 | from the zf-documentation repo, and publish it to 166 | https://zendframework.github.io/zend-mime/ 167 | 168 | ### Deprecated 169 | 170 | - Nothing. 171 | 172 | ### Removed 173 | 174 | - Nothing. 175 | 176 | ### Fixed 177 | 178 | - [#2](https://github.com/zendframework/zend-mime/pull/2) fixes 179 | `Mime::encodeBase64()`'s behavior when presented with lines of invalid 180 | lengths (not multiples of 4). 181 | - [#4](https://github.com/zendframework/zend-mime/pull/4) modifies 182 | `Mime::encodeQuotedPrintable()` to ensure it never creates a header line 183 | consisting of only a dot (concatenation character), a situation that can break 184 | parsing by Outlook. 185 | - [#7](https://github.com/zendframework/zend-mime/pull/7) provides a patch that 186 | allows parsing MIME parts that have no headers. 187 | - [#9](https://github.com/zendframework/zend-mime/pull/9) updates the 188 | dependencies to: 189 | - allow PHP 5.5+ or PHP 7+ versions. 190 | - allow zend-stdlib 2.7+ or 3.0+ verions. 191 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2019, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-mime 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [laminas/laminas-mime](https://github.com/laminas/laminas-mime). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-mime.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-mime) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-mime/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-mime?branch=master) 9 | 10 | `Zend\Mime` is a support class for handling multipart MIME messages. It is used 11 | by `Zend\Mail` and `Zend\Mime\Message` and may be used by applications requiring 12 | MIME support. 13 | 14 | ## Installation 15 | 16 | Run the following to install this library: 17 | 18 | ```bash 19 | $ composer require zendframework/zend-mime 20 | ``` 21 | 22 | ## Documentation 23 | 24 | Browse the documentation online at https://docs.zendframework.com/zend-mime/ 25 | 26 | ## Support 27 | 28 | * [Issues](https://github.com/zendframework/zend-mime/issues/) 29 | * [Chat](https://zendframework-slack.herokuapp.com/) 30 | * [Forum](https://discourse.zendframework.com/) 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-mime", 3 | "description": "Create and parse MIME messages and parts", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zendframework", 7 | "zf", 8 | "mime" 9 | ], 10 | "support": { 11 | "docs": "https://docs.zendframework.com/zend-mime/", 12 | "issues": "https://github.com/zendframework/zend-mime/issues", 13 | "source": "https://github.com/zendframework/zend-mime", 14 | "rss": "https://github.com/zendframework/zend-mime/releases.atom", 15 | "chat": "https://zendframework-slack.herokuapp.com", 16 | "forum": "https://discourse.zendframework.com/c/questions/components" 17 | }, 18 | "require": { 19 | "php": "^5.6 || ^7.0", 20 | "zendframework/zend-stdlib": "^2.7 || ^3.0" 21 | }, 22 | "require-dev": { 23 | "zendframework/zend-mail": "^2.6", 24 | "phpunit/phpunit": "^5.7.21 || ^6.3", 25 | "zendframework/zend-coding-standard": "~1.0.0" 26 | }, 27 | "suggest": { 28 | "zendframework/zend-mail": "Zend\\Mail component" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Zend\\Mime\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "ZendTest\\Mime\\": "test/" 38 | } 39 | }, 40 | "config": { 41 | "sort-packages": true 42 | }, 43 | "extra": { 44 | "branch-alias": { 45 | "dev-master": "2.7.x-dev", 46 | "dev-develop": "2.8.x-dev" 47 | } 48 | }, 49 | "scripts": { 50 | "check": [ 51 | "@cs-check", 52 | "@test" 53 | ], 54 | "cs-check": "phpcs", 55 | "cs-fix": "phpcbf", 56 | "test": "phpunit --colors=always", 57 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Decode.php: -------------------------------------------------------------------------------- 1 | array(name => value), 'body' => content), null if no parts found 68 | * @throws Exception\RuntimeException 69 | */ 70 | public static function splitMessageStruct($message, $boundary, $EOL = Mime::LINEEND) 71 | { 72 | $parts = static::splitMime($message, $boundary); 73 | if (! $parts) { 74 | return; 75 | } 76 | $result = []; 77 | $headers = null; // "Declare" variable before the first usage "for reading" 78 | $body = null; // "Declare" variable before the first usage "for reading" 79 | foreach ($parts as $part) { 80 | static::splitMessage($part, $headers, $body, $EOL); 81 | $result[] = [ 82 | 'header' => $headers, 83 | 'body' => $body, 84 | ]; 85 | } 86 | return $result; 87 | } 88 | 89 | /** 90 | * split a message in header and body part, if no header or an 91 | * invalid header is found $headers is empty 92 | * 93 | * The charset of the returned headers depend on your iconv settings. 94 | * 95 | * @param string|Headers $message raw message with header and optional content 96 | * @param Headers $headers output param, headers container 97 | * @param string $body output param, content of message 98 | * @param string $EOL EOL string; defaults to {@link Zend\Mime\Mime::LINEEND} 99 | * @param bool $strict enable strict mode for parsing message 100 | * @return null 101 | */ 102 | public static function splitMessage($message, &$headers, &$body, $EOL = Mime::LINEEND, $strict = false) 103 | { 104 | if ($message instanceof Headers) { 105 | $message = $message->toString(); 106 | } 107 | // check for valid header at first line 108 | $firstlinePos = strpos($message, "\n"); 109 | $firstline = $firstlinePos === false ? $message : substr($message, 0, $firstlinePos); 110 | if (! preg_match('%^[^\s]+[^:]*:%', $firstline)) { 111 | $headers = new Headers(); 112 | // TODO: we're ignoring \r for now - is this function fast enough and is it safe to assume noone needs \r? 113 | $body = str_replace(["\r", "\n"], ['', $EOL], $message); 114 | return; 115 | } 116 | 117 | // see @ZF2-372, pops the first line off a message if it doesn't contain a header 118 | if (! $strict) { 119 | $parts = explode(':', $firstline, 2); 120 | if (count($parts) != 2) { 121 | $message = substr($message, strpos($message, $EOL) + 1); 122 | } 123 | } 124 | 125 | // @todo splitMime removes "\r" sequences, which breaks valid mime 126 | // messages as returned by many mail servers 127 | $headersEOL = $EOL; 128 | 129 | // find an empty line between headers and body 130 | // default is set new line 131 | // @todo Maybe this is too much "magic"; we should be more strict here 132 | if (strpos($message, $EOL . $EOL)) { 133 | list($headers, $body) = explode($EOL . $EOL, $message, 2); 134 | // next is the standard new line 135 | } elseif ($EOL != "\r\n" && strpos($message, "\r\n\r\n")) { 136 | list($headers, $body) = explode("\r\n\r\n", $message, 2); 137 | $headersEOL = "\r\n"; // Headers::fromString will fail with incorrect EOL 138 | // next is the other "standard" new line 139 | } elseif ($EOL != "\n" && strpos($message, "\n\n")) { 140 | list($headers, $body) = explode("\n\n", $message, 2); 141 | $headersEOL = "\n"; 142 | // at last resort find anything that looks like a new line 143 | } else { 144 | ErrorHandler::start(E_NOTICE | E_WARNING); 145 | list($headers, $body) = preg_split("%([\r\n]+)\\1%U", $message, 2); 146 | ErrorHandler::stop(); 147 | } 148 | 149 | $headers = Headers::fromString($headers, $headersEOL); 150 | } 151 | 152 | /** 153 | * split a content type in its different parts 154 | * 155 | * @param string $type content-type 156 | * @param string $wantedPart the wanted part, else an array with all parts is returned 157 | * @return string|array wanted part or all parts as array('type' => content-type, partname => value) 158 | */ 159 | public static function splitContentType($type, $wantedPart = null) 160 | { 161 | return static::splitHeaderField($type, $wantedPart, 'type'); 162 | } 163 | 164 | /** 165 | * split a header field like content type in its different parts 166 | * 167 | * @param string $field header field 168 | * @param string $wantedPart the wanted part, else an array with all parts is returned 169 | * @param string $firstName key name for the first part 170 | * @return string|array wanted part or all parts as array($firstName => firstPart, partname => value) 171 | * @throws Exception\RuntimeException 172 | */ 173 | public static function splitHeaderField($field, $wantedPart = null, $firstName = '0') 174 | { 175 | $wantedPart = strtolower($wantedPart); 176 | $firstName = strtolower($firstName); 177 | 178 | // special case - a bit optimized 179 | if ($firstName === $wantedPart) { 180 | $field = strtok($field, ';'); 181 | return $field[0] == '"' ? substr($field, 1, -1) : $field; 182 | } 183 | 184 | $field = $firstName . '=' . $field; 185 | if (! preg_match_all('%([^=\s]+)\s*=\s*("[^"]+"|[^;]+)(;\s*|$)%', $field, $matches)) { 186 | throw new Exception\RuntimeException('not a valid header field'); 187 | } 188 | 189 | if ($wantedPart) { 190 | foreach ($matches[1] as $key => $name) { 191 | if (strcasecmp($name, $wantedPart)) { 192 | continue; 193 | } 194 | if ($matches[2][$key][0] != '"') { 195 | return $matches[2][$key]; 196 | } 197 | return substr($matches[2][$key], 1, -1); 198 | } 199 | return; 200 | } 201 | 202 | $split = []; 203 | foreach ($matches[1] as $key => $name) { 204 | $name = strtolower($name); 205 | if ($matches[2][$key][0] == '"') { 206 | $split[$name] = substr($matches[2][$key], 1, -1); 207 | } else { 208 | $split[$name] = $matches[2][$key]; 209 | } 210 | } 211 | 212 | return $split; 213 | } 214 | 215 | /** 216 | * decode a quoted printable encoded string 217 | * 218 | * The charset of the returned string depends on your iconv settings. 219 | * 220 | * @param string $string encoded string 221 | * @return string decoded string 222 | */ 223 | public static function decodeQuotedPrintable($string) 224 | { 225 | return iconv_mime_decode($string, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8'); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | parts; 23 | } 24 | 25 | /** 26 | * Sets the given array of Zend\Mime\Part as the array for the message 27 | * 28 | * @param array $parts 29 | * @return self 30 | */ 31 | public function setParts($parts) 32 | { 33 | $this->parts = $parts; 34 | return $this; 35 | } 36 | 37 | /** 38 | * Append a new Zend\Mime\Part to the current message 39 | * 40 | * @param \Zend\Mime\Part $part 41 | * @throws Exception\InvalidArgumentException 42 | * @return self 43 | */ 44 | public function addPart(Part $part) 45 | { 46 | foreach ($this->getParts() as $key => $row) { 47 | if ($part == $row) { 48 | throw new Exception\InvalidArgumentException(sprintf( 49 | 'Provided part %s already defined.', 50 | $part->getId() 51 | )); 52 | } 53 | } 54 | 55 | $this->parts[] = $part; 56 | return $this; 57 | } 58 | 59 | /** 60 | * Check if message needs to be sent as multipart 61 | * MIME message or if it has only one part. 62 | * 63 | * @return bool 64 | */ 65 | public function isMultiPart() 66 | { 67 | return (count($this->parts) > 1); 68 | } 69 | 70 | /** 71 | * Set Zend\Mime\Mime object for the message 72 | * 73 | * This can be used to set the boundary specifically or to use a subclass of 74 | * Zend\Mime for generating the boundary. 75 | * 76 | * @param \Zend\Mime\Mime $mime 77 | * @return self 78 | */ 79 | public function setMime(Mime $mime) 80 | { 81 | $this->mime = $mime; 82 | return $this; 83 | } 84 | 85 | /** 86 | * Returns the Zend\Mime\Mime object in use by the message 87 | * 88 | * If the object was not present, it is created and returned. Can be used to 89 | * determine the boundary used in this message. 90 | * 91 | * @return \Zend\Mime\Mime 92 | */ 93 | public function getMime() 94 | { 95 | if ($this->mime === null) { 96 | $this->mime = new Mime(); 97 | } 98 | 99 | return $this->mime; 100 | } 101 | 102 | /** 103 | * Generate MIME-compliant message from the current configuration 104 | * 105 | * This can be a multipart message if more than one MIME part was added. If 106 | * only one part is present, the content of this part is returned. If no 107 | * part had been added, an empty string is returned. 108 | * 109 | * Parts are separated by the mime boundary as defined in Zend\Mime\Mime. If 110 | * {@link setMime()} has been called before this method, the Zend\Mime\Mime 111 | * object set by this call will be used. Otherwise, a new Zend\Mime\Mime object 112 | * is generated and used. 113 | * 114 | * @param string $EOL EOL string; defaults to {@link Zend\Mime\Mime::LINEEND} 115 | * @return string 116 | */ 117 | public function generateMessage($EOL = Mime::LINEEND) 118 | { 119 | if (! $this->isMultiPart()) { 120 | if (empty($this->parts)) { 121 | return ''; 122 | } 123 | $part = current($this->parts); 124 | $body = $part->getContent($EOL); 125 | } else { 126 | $mime = $this->getMime(); 127 | 128 | $boundaryLine = $mime->boundaryLine($EOL); 129 | $body = 'This is a message in Mime Format. If you see this, ' 130 | . "your mail reader does not support this format." . $EOL; 131 | 132 | foreach (array_keys($this->parts) as $p) { 133 | $body .= $boundaryLine 134 | . $this->getPartHeaders($p, $EOL) 135 | . $EOL 136 | . $this->getPartContent($p, $EOL); 137 | } 138 | 139 | $body .= $mime->mimeEnd($EOL); 140 | } 141 | 142 | return trim($body); 143 | } 144 | 145 | /** 146 | * Get the headers of a given part as an array 147 | * 148 | * @param int $partnum 149 | * @return array 150 | */ 151 | public function getPartHeadersArray($partnum) 152 | { 153 | return $this->parts[$partnum]->getHeadersArray(); 154 | } 155 | 156 | /** 157 | * Get the headers of a given part as a string 158 | * 159 | * @param int $partnum 160 | * @param string $EOL 161 | * @return string 162 | */ 163 | public function getPartHeaders($partnum, $EOL = Mime::LINEEND) 164 | { 165 | return $this->parts[$partnum]->getHeaders($EOL); 166 | } 167 | 168 | /** 169 | * Get the (encoded) content of a given part as a string 170 | * 171 | * @param int $partnum 172 | * @param string $EOL 173 | * @return string 174 | */ 175 | public function getPartContent($partnum, $EOL = Mime::LINEEND) 176 | { 177 | return $this->parts[$partnum]->getContent($EOL); 178 | } 179 | 180 | /** 181 | * Explode MIME multipart string into separate parts 182 | * 183 | * Parts consist of the header and the body of each MIME part. 184 | * 185 | * @param string $body 186 | * @param string $boundary 187 | * @throws Exception\RuntimeException 188 | * @return array 189 | */ 190 | // @codingStandardsIgnoreStart 191 | protected static function _disassembleMime($body, $boundary) 192 | { 193 | // @codingStandardsIgnoreEnd 194 | $start = 0; 195 | $res = []; 196 | // find every mime part limiter and cut out the 197 | // string before it. 198 | // the part before the first boundary string is discarded: 199 | $p = strpos($body, '--' . $boundary."\n", $start); 200 | if ($p === false) { 201 | // no parts found! 202 | return []; 203 | } 204 | 205 | // position after first boundary line 206 | $start = $p + 3 + strlen($boundary); 207 | 208 | while (($p = strpos($body, '--' . $boundary . "\n", $start)) !== false) { 209 | $res[] = substr($body, $start, $p - $start); 210 | $start = $p + 3 + strlen($boundary); 211 | } 212 | 213 | // no more parts, find end boundary 214 | $p = strpos($body, '--' . $boundary . '--', $start); 215 | if ($p === false) { 216 | throw new Exception\RuntimeException('Not a valid Mime Message: End Missing'); 217 | } 218 | 219 | // the remaining part also needs to be parsed: 220 | $res[] = substr($body, $start, $p - $start); 221 | return $res; 222 | } 223 | 224 | /** 225 | * Decodes a MIME encoded string and returns a Zend\Mime\Message object with 226 | * all the MIME parts set according to the given string 227 | * 228 | * @param string $message 229 | * @param string $boundary Multipart boundary; if omitted, $message will be 230 | * treated as a single part. 231 | * @param string $EOL EOL string; defaults to {@link Zend\Mime\Mime::LINEEND} 232 | * @throws Exception\RuntimeException 233 | * @return Message 234 | */ 235 | public static function createFromMessage($message, $boundary = null, $EOL = Mime::LINEEND) 236 | { 237 | if ($boundary) { 238 | $parts = Decode::splitMessageStruct($message, $boundary, $EOL); 239 | } else { 240 | Decode::splitMessage($message, $headers, $body, $EOL); 241 | $parts = [[ 242 | 'header' => $headers, 243 | 'body' => $body, 244 | ]]; 245 | } 246 | 247 | $res = new static(); 248 | foreach ($parts as $part) { 249 | // now we build a new MimePart for the current Message Part: 250 | $properties = []; 251 | foreach ($part['header'] as $header) { 252 | /** @var \Zend\Mail\Header\HeaderInterface $header */ 253 | /** 254 | * @todo check for characterset and filename 255 | */ 256 | 257 | $fieldName = $header->getFieldName(); 258 | $fieldValue = $header->getFieldValue(); 259 | switch (strtolower($fieldName)) { 260 | case 'content-type': 261 | $properties['type'] = $fieldValue; 262 | break; 263 | case 'content-transfer-encoding': 264 | $properties['encoding'] = $fieldValue; 265 | break; 266 | case 'content-id': 267 | $properties['id'] = trim($fieldValue, '<>'); 268 | break; 269 | case 'content-disposition': 270 | $properties['disposition'] = $fieldValue; 271 | break; 272 | case 'content-description': 273 | $properties['description'] = $fieldValue; 274 | break; 275 | case 'content-location': 276 | $properties['location'] = $fieldValue; 277 | break; 278 | case 'content-language': 279 | $properties['language'] = $fieldValue; 280 | break; 281 | default: 282 | // Ignore unknown header 283 | break; 284 | } 285 | } 286 | 287 | $body = $part['body']; 288 | 289 | if (isset($properties['encoding'])) { 290 | switch ($properties['encoding']) { 291 | case 'quoted-printable': 292 | $body = quoted_printable_decode($body); 293 | break; 294 | case 'base64': 295 | $body = base64_decode($body); 296 | break; 297 | } 298 | } 299 | 300 | $newPart = new Part($body); 301 | foreach ($properties as $key => $value) { 302 | $newPart->$key = $value; 303 | } 304 | $res->addPart($newPart); 305 | } 306 | 307 | return $res; 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/Mime.php: -------------------------------------------------------------------------------- 1 | [\x21\x23-\x26\x2a\x2b\x2d\x5e\5f\60\x7b-\x7ea-zA-Z0-9]+)\?(?P[\x21\x23-\x26\x2a\x2b\x2d\x5e\5f\60\x7b-\x7ea-zA-Z0-9]+)\?(?P[\x21-\x3e\x40-\x7e]+)#'; 31 | // @codingStandardsIgnoreEnd 32 | 33 | protected $boundary; 34 | protected static $makeUnique = 0; 35 | 36 | // lookup-Tables for QuotedPrintable 37 | public static $qpKeys = [ 38 | "\x00","\x01","\x02","\x03","\x04","\x05","\x06","\x07", 39 | "\x08","\x09","\x0A","\x0B","\x0C","\x0D","\x0E","\x0F", 40 | "\x10","\x11","\x12","\x13","\x14","\x15","\x16","\x17", 41 | "\x18","\x19","\x1A","\x1B","\x1C","\x1D","\x1E","\x1F", 42 | "\x7F","\x80","\x81","\x82","\x83","\x84","\x85","\x86", 43 | "\x87","\x88","\x89","\x8A","\x8B","\x8C","\x8D","\x8E", 44 | "\x8F","\x90","\x91","\x92","\x93","\x94","\x95","\x96", 45 | "\x97","\x98","\x99","\x9A","\x9B","\x9C","\x9D","\x9E", 46 | "\x9F","\xA0","\xA1","\xA2","\xA3","\xA4","\xA5","\xA6", 47 | "\xA7","\xA8","\xA9","\xAA","\xAB","\xAC","\xAD","\xAE", 48 | "\xAF","\xB0","\xB1","\xB2","\xB3","\xB4","\xB5","\xB6", 49 | "\xB7","\xB8","\xB9","\xBA","\xBB","\xBC","\xBD","\xBE", 50 | "\xBF","\xC0","\xC1","\xC2","\xC3","\xC4","\xC5","\xC6", 51 | "\xC7","\xC8","\xC9","\xCA","\xCB","\xCC","\xCD","\xCE", 52 | "\xCF","\xD0","\xD1","\xD2","\xD3","\xD4","\xD5","\xD6", 53 | "\xD7","\xD8","\xD9","\xDA","\xDB","\xDC","\xDD","\xDE", 54 | "\xDF","\xE0","\xE1","\xE2","\xE3","\xE4","\xE5","\xE6", 55 | "\xE7","\xE8","\xE9","\xEA","\xEB","\xEC","\xED","\xEE", 56 | "\xEF","\xF0","\xF1","\xF2","\xF3","\xF4","\xF5","\xF6", 57 | "\xF7","\xF8","\xF9","\xFA","\xFB","\xFC","\xFD","\xFE", 58 | "\xFF" 59 | ]; 60 | 61 | public static $qpReplaceValues = [ 62 | "=00","=01","=02","=03","=04","=05","=06","=07", 63 | "=08","=09","=0A","=0B","=0C","=0D","=0E","=0F", 64 | "=10","=11","=12","=13","=14","=15","=16","=17", 65 | "=18","=19","=1A","=1B","=1C","=1D","=1E","=1F", 66 | "=7F","=80","=81","=82","=83","=84","=85","=86", 67 | "=87","=88","=89","=8A","=8B","=8C","=8D","=8E", 68 | "=8F","=90","=91","=92","=93","=94","=95","=96", 69 | "=97","=98","=99","=9A","=9B","=9C","=9D","=9E", 70 | "=9F","=A0","=A1","=A2","=A3","=A4","=A5","=A6", 71 | "=A7","=A8","=A9","=AA","=AB","=AC","=AD","=AE", 72 | "=AF","=B0","=B1","=B2","=B3","=B4","=B5","=B6", 73 | "=B7","=B8","=B9","=BA","=BB","=BC","=BD","=BE", 74 | "=BF","=C0","=C1","=C2","=C3","=C4","=C5","=C6", 75 | "=C7","=C8","=C9","=CA","=CB","=CC","=CD","=CE", 76 | "=CF","=D0","=D1","=D2","=D3","=D4","=D5","=D6", 77 | "=D7","=D8","=D9","=DA","=DB","=DC","=DD","=DE", 78 | "=DF","=E0","=E1","=E2","=E3","=E4","=E5","=E6", 79 | "=E7","=E8","=E9","=EA","=EB","=EC","=ED","=EE", 80 | "=EF","=F0","=F1","=F2","=F3","=F4","=F5","=F6", 81 | "=F7","=F8","=F9","=FA","=FB","=FC","=FD","=FE", 82 | "=FF" 83 | ]; 84 | // @codingStandardsIgnoreStart 85 | public static $qpKeysString = 86 | "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF"; 87 | // @codingStandardsIgnoreEnd 88 | 89 | /** 90 | * Check if the given string is "printable" 91 | * 92 | * Checks that a string contains no unprintable characters. If this returns 93 | * false, encode the string for secure delivery. 94 | * 95 | * @param string $str 96 | * @return bool 97 | */ 98 | public static function isPrintable($str) 99 | { 100 | return (strcspn($str, static::$qpKeysString) == strlen($str)); 101 | } 102 | 103 | /** 104 | * Encode a given string with the QUOTED_PRINTABLE mechanism and wrap the lines. 105 | * 106 | * @param string $str 107 | * @param int $lineLength Defaults to {@link LINELENGTH} 108 | * @param string $lineEnd Defaults to {@link LINEEND} 109 | * @return string 110 | */ 111 | public static function encodeQuotedPrintable( 112 | $str, 113 | $lineLength = self::LINELENGTH, 114 | $lineEnd = self::LINEEND 115 | ) { 116 | $out = ''; 117 | $str = self::_encodeQuotedPrintable($str); 118 | 119 | // Split encoded text into separate lines 120 | while ($str) { 121 | $ptr = strlen($str); 122 | if ($ptr > $lineLength) { 123 | $ptr = $lineLength; 124 | } 125 | 126 | // Ensure we are not splitting across an encoded character 127 | $pos = strrpos(substr($str, 0, $ptr), '='); 128 | if ($pos !== false && $pos >= $ptr - 2) { 129 | $ptr = $pos; 130 | } 131 | 132 | if (ord($str[0]) == 0x2E) { // 0x2E is a dot 133 | $str = '=2E' . substr($str, 1); 134 | $ptr += 2; 135 | } 136 | 137 | // copied from swiftmailer https://git.io/vAXU1 138 | switch (ord(substr($str, $ptr - 1))) { 139 | case 0x09: // Horizontal Tab 140 | $str = substr_replace($str, '=09', $ptr - 1, 1); 141 | $ptr += 2; 142 | break; 143 | case 0x20: // Space 144 | $str = substr_replace($str, '=20', $ptr - 1, 1); 145 | $ptr += 2; 146 | break; 147 | } 148 | 149 | // Add string and continue 150 | $out .= substr($str, 0, $ptr) . '=' . $lineEnd; 151 | $str = substr($str, $ptr); 152 | } 153 | 154 | $out = rtrim($out, $lineEnd); 155 | $out = rtrim($out, '='); 156 | return $out; 157 | } 158 | 159 | /** 160 | * Converts a string into quoted printable format. 161 | * 162 | * @param string $str 163 | * @return string 164 | */ 165 | // @codingStandardsIgnoreStart 166 | private static function _encodeQuotedPrintable($str) 167 | { 168 | // @codingStandardsIgnoreEnd 169 | $str = str_replace('=', '=3D', $str); 170 | $str = str_replace(static::$qpKeys, static::$qpReplaceValues, $str); 171 | $str = rtrim($str); 172 | return $str; 173 | } 174 | 175 | /** 176 | * Encode a given string with the QUOTED_PRINTABLE mechanism for Mail Headers. 177 | * 178 | * Mail headers depend on an extended quoted printable algorithm otherwise 179 | * a range of bugs can occur. 180 | * 181 | * @param string $str 182 | * @param string $charset 183 | * @param int $lineLength Defaults to {@link LINELENGTH} 184 | * @param string $lineEnd Defaults to {@link LINEEND} 185 | * @return string 186 | */ 187 | public static function encodeQuotedPrintableHeader( 188 | $str, 189 | $charset, 190 | $lineLength = self::LINELENGTH, 191 | $lineEnd = self::LINEEND 192 | ) { 193 | // Reduce line-length by the length of the required delimiter, charsets and encoding 194 | $prefix = sprintf('=?%s?Q?', $charset); 195 | $lineLength = $lineLength - strlen($prefix) - 3; 196 | 197 | $str = self::_encodeQuotedPrintable($str); 198 | 199 | // Mail-Header required chars have to be encoded also: 200 | $str = str_replace(['?', ',', ' ', '_'], ['=3F', '=2C', '=20', '=5F'], $str); 201 | 202 | // initialize first line, we need it anyways 203 | $lines = [0 => '']; 204 | 205 | // Split encoded text into separate lines 206 | $tmp = ''; 207 | while (strlen($str) > 0) { 208 | $currentLine = max(count($lines) - 1, 0); 209 | $token = static::getNextQuotedPrintableToken($str); 210 | $substr = substr($str, strlen($token)); 211 | $str = (false === $substr) ? '' : $substr; 212 | 213 | $tmp .= $token; 214 | if ($token === '=20') { 215 | // only if we have a single char token or space, we can append the 216 | // tempstring it to the current line or start a new line if necessary. 217 | $lineLimitReached = (strlen($lines[$currentLine] . $tmp) > $lineLength); 218 | $noCurrentLine = ($lines[$currentLine] === ''); 219 | if ($noCurrentLine && $lineLimitReached) { 220 | $lines[$currentLine] = $tmp; 221 | $lines[$currentLine + 1] = ''; 222 | } elseif ($lineLimitReached) { 223 | $lines[$currentLine + 1] = $tmp; 224 | } else { 225 | $lines[$currentLine] .= $tmp; 226 | } 227 | $tmp = ''; 228 | } 229 | // don't forget to append the rest to the last line 230 | if (strlen($str) === 0) { 231 | $lines[$currentLine] .= $tmp; 232 | } 233 | } 234 | 235 | // assemble the lines together by pre- and appending delimiters, charset, encoding. 236 | for ($i = 0, $count = count($lines); $i < $count; $i++) { 237 | $lines[$i] = " " . $prefix . $lines[$i] . "?="; 238 | } 239 | $str = trim(implode($lineEnd, $lines)); 240 | return $str; 241 | } 242 | 243 | /** 244 | * Retrieves the first token from a quoted printable string. 245 | * 246 | * @param string $str 247 | * @return string 248 | */ 249 | private static function getNextQuotedPrintableToken($str) 250 | { 251 | if (0 === strpos($str, '=')) { 252 | $token = substr($str, 0, 3); 253 | } else { 254 | $token = substr($str, 0, 1); 255 | } 256 | return $token; 257 | } 258 | 259 | /** 260 | * Encode a given string in mail header compatible base64 encoding. 261 | * 262 | * @param string $str 263 | * @param string $charset 264 | * @param int $lineLength Defaults to {@link LINELENGTH} 265 | * @param string $lineEnd Defaults to {@link LINEEND} 266 | * @return string 267 | */ 268 | public static function encodeBase64Header( 269 | $str, 270 | $charset, 271 | $lineLength = self::LINELENGTH, 272 | $lineEnd = self::LINEEND 273 | ) { 274 | $prefix = '=?' . $charset . '?B?'; 275 | $suffix = '?='; 276 | $remainingLength = $lineLength - strlen($prefix) - strlen($suffix); 277 | 278 | $encodedValue = static::encodeBase64($str, $remainingLength, $lineEnd); 279 | $encodedValue = str_replace($lineEnd, $suffix . $lineEnd . ' ' . $prefix, $encodedValue); 280 | $encodedValue = $prefix . $encodedValue . $suffix; 281 | return $encodedValue; 282 | } 283 | 284 | /** 285 | * Encode a given string in base64 encoding and break lines 286 | * according to the maximum linelength. 287 | * 288 | * @param string $str 289 | * @param int $lineLength Defaults to {@link LINELENGTH} 290 | * @param string $lineEnd Defaults to {@link LINEEND} 291 | * @return string 292 | */ 293 | public static function encodeBase64( 294 | $str, 295 | $lineLength = self::LINELENGTH, 296 | $lineEnd = self::LINEEND 297 | ) { 298 | $lineLength = $lineLength - ($lineLength % 4); 299 | return rtrim(chunk_split(base64_encode($str), $lineLength, $lineEnd)); 300 | } 301 | 302 | /** 303 | * Constructor 304 | * 305 | * @param null|string $boundary 306 | * @access public 307 | */ 308 | public function __construct($boundary = null) 309 | { 310 | // This string needs to be somewhat unique 311 | if ($boundary === null) { 312 | $this->boundary = '=_' . md5(microtime(1) . static::$makeUnique++); 313 | } else { 314 | $this->boundary = $boundary; 315 | } 316 | } 317 | 318 | /** 319 | * Encode the given string with the given encoding. 320 | * 321 | * @param string $str 322 | * @param string $encoding 323 | * @param string $EOL EOL string; defaults to {@link LINEEND} 324 | * @return string 325 | */ 326 | public static function encode($str, $encoding, $EOL = self::LINEEND) 327 | { 328 | switch ($encoding) { 329 | case self::ENCODING_BASE64: 330 | return static::encodeBase64($str, self::LINELENGTH, $EOL); 331 | 332 | case self::ENCODING_QUOTEDPRINTABLE: 333 | return static::encodeQuotedPrintable($str, self::LINELENGTH, $EOL); 334 | 335 | default: 336 | /** 337 | * @todo 7Bit and 8Bit is currently handled the same way. 338 | */ 339 | return $str; 340 | } 341 | } 342 | 343 | /** 344 | * Return a MIME boundary 345 | * 346 | * @access public 347 | * @return string 348 | */ 349 | public function boundary() 350 | { 351 | return $this->boundary; 352 | } 353 | 354 | /** 355 | * Return a MIME boundary line 356 | * 357 | * @param string $EOL Defaults to {@link LINEEND} 358 | * @access public 359 | * @return string 360 | */ 361 | public function boundaryLine($EOL = self::LINEEND) 362 | { 363 | return $EOL . '--' . $this->boundary . $EOL; 364 | } 365 | 366 | /** 367 | * Return MIME ending 368 | * 369 | * @param string $EOL Defaults to {@link LINEEND} 370 | * @access public 371 | * @return string 372 | */ 373 | public function mimeEnd($EOL = self::LINEEND) 374 | { 375 | return $EOL . '--' . $this->boundary . '--' . $EOL; 376 | } 377 | 378 | /** 379 | * Detect MIME charset 380 | * 381 | * Extract parts according to https://tools.ietf.org/html/rfc2047#section-2 382 | * 383 | * @param string $str 384 | * @return string 385 | */ 386 | public static function mimeDetectCharset($str) 387 | { 388 | if (preg_match(self::CHARSET_REGEX, $str, $matches)) { 389 | return strtoupper($matches['charset']); 390 | } 391 | 392 | return 'ASCII'; 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/Part.php: -------------------------------------------------------------------------------- 1 | setContent($content); 40 | } 41 | 42 | /** 43 | * @todo error checking for setting $type 44 | * @todo error checking for setting $encoding 45 | */ 46 | 47 | /** 48 | * Set type 49 | * @param string $type 50 | * @return self 51 | */ 52 | public function setType($type = Mime::TYPE_OCTETSTREAM) 53 | { 54 | $this->type = $type; 55 | return $this; 56 | } 57 | 58 | /** 59 | * Get type 60 | * @return string 61 | */ 62 | public function getType() 63 | { 64 | return $this->type; 65 | } 66 | 67 | /** 68 | * Set encoding 69 | * @param string $encoding 70 | * @return self 71 | */ 72 | public function setEncoding($encoding = Mime::ENCODING_8BIT) 73 | { 74 | $this->encoding = $encoding; 75 | return $this; 76 | } 77 | 78 | /** 79 | * Get encoding 80 | * @return string 81 | */ 82 | public function getEncoding() 83 | { 84 | return $this->encoding; 85 | } 86 | 87 | /** 88 | * Set id 89 | * @param string $id 90 | * @return self 91 | */ 92 | public function setId($id) 93 | { 94 | $this->id = $id; 95 | return $this; 96 | } 97 | 98 | /** 99 | * Get id 100 | * @return string 101 | */ 102 | public function getId() 103 | { 104 | return $this->id; 105 | } 106 | 107 | /** 108 | * Set disposition 109 | * @param string $disposition 110 | * @return self 111 | */ 112 | public function setDisposition($disposition) 113 | { 114 | $this->disposition = $disposition; 115 | return $this; 116 | } 117 | 118 | /** 119 | * Get disposition 120 | * @return string 121 | */ 122 | public function getDisposition() 123 | { 124 | return $this->disposition; 125 | } 126 | 127 | /** 128 | * Set description 129 | * @param string $description 130 | * @return self 131 | */ 132 | public function setDescription($description) 133 | { 134 | $this->description = $description; 135 | return $this; 136 | } 137 | 138 | /** 139 | * Get description 140 | * @return string 141 | */ 142 | public function getDescription() 143 | { 144 | return $this->description; 145 | } 146 | 147 | /** 148 | * Set filename 149 | * @param string $fileName 150 | * @return self 151 | */ 152 | public function setFileName($fileName) 153 | { 154 | $this->filename = $fileName; 155 | return $this; 156 | } 157 | 158 | /** 159 | * Get filename 160 | * @return string 161 | */ 162 | public function getFileName() 163 | { 164 | return $this->filename; 165 | } 166 | 167 | /** 168 | * Set charset 169 | * @param string $type 170 | * @return self 171 | */ 172 | public function setCharset($charset) 173 | { 174 | $this->charset = $charset; 175 | return $this; 176 | } 177 | 178 | /** 179 | * Get charset 180 | * @return string 181 | */ 182 | public function getCharset() 183 | { 184 | return $this->charset; 185 | } 186 | 187 | /** 188 | * Set boundary 189 | * @param string $boundary 190 | * @return self 191 | */ 192 | public function setBoundary($boundary) 193 | { 194 | $this->boundary = $boundary; 195 | return $this; 196 | } 197 | 198 | /** 199 | * Get boundary 200 | * @return string 201 | */ 202 | public function getBoundary() 203 | { 204 | return $this->boundary; 205 | } 206 | 207 | /** 208 | * Set location 209 | * @param string $location 210 | * @return self 211 | */ 212 | public function setLocation($location) 213 | { 214 | $this->location = $location; 215 | return $this; 216 | } 217 | 218 | /** 219 | * Get location 220 | * @return string 221 | */ 222 | public function getLocation() 223 | { 224 | return $this->location; 225 | } 226 | 227 | /** 228 | * Set language 229 | * @param string $language 230 | * @return self 231 | */ 232 | public function setLanguage($language) 233 | { 234 | $this->language = $language; 235 | return $this; 236 | } 237 | 238 | /** 239 | * Get language 240 | * @return string 241 | */ 242 | public function getLanguage() 243 | { 244 | return $this->language; 245 | } 246 | 247 | /** 248 | * Set content 249 | * @param mixed $content String or Stream containing the content 250 | * @throws Exception\InvalidArgumentException 251 | * @return self 252 | */ 253 | public function setContent($content) 254 | { 255 | if (! is_string($content) && ! is_resource($content)) { 256 | throw new Exception\InvalidArgumentException(sprintf( 257 | 'Content must be string or resource; received "%s"', 258 | is_object($content) ? get_class($content) : gettype($content) 259 | )); 260 | } 261 | $this->content = $content; 262 | if (is_resource($content)) { 263 | $this->isStream = true; 264 | } 265 | 266 | return $this; 267 | } 268 | 269 | /** 270 | * Set isStream 271 | * @param bool $isStream 272 | * @return self 273 | */ 274 | public function setIsStream($isStream = false) 275 | { 276 | $this->isStream = (bool) $isStream; 277 | return $this; 278 | } 279 | 280 | /** 281 | * Get isStream 282 | * @return bool 283 | */ 284 | public function getIsStream() 285 | { 286 | return $this->isStream; 287 | } 288 | 289 | /** 290 | * Set filters 291 | * @param array $filters 292 | * @return self 293 | */ 294 | public function setFilters($filters = []) 295 | { 296 | $this->filters = $filters; 297 | return $this; 298 | } 299 | 300 | /** 301 | * Get Filters 302 | * @return array 303 | */ 304 | public function getFilters() 305 | { 306 | return $this->filters; 307 | } 308 | 309 | /** 310 | * check if this part can be read as a stream. 311 | * if true, getEncodedStream can be called, otherwise 312 | * only getContent can be used to fetch the encoded 313 | * content of the part 314 | * 315 | * @return bool 316 | */ 317 | public function isStream() 318 | { 319 | return $this->isStream; 320 | } 321 | 322 | /** 323 | * if this was created with a stream, return a filtered stream for 324 | * reading the content. very useful for large file attachments. 325 | * 326 | * @param string $EOL 327 | * @return resource 328 | * @throws Exception\RuntimeException if not a stream or unable to append filter 329 | */ 330 | public function getEncodedStream($EOL = Mime::LINEEND) 331 | { 332 | if (! $this->isStream) { 333 | throw new Exception\RuntimeException('Attempt to get a stream from a string part'); 334 | } 335 | 336 | //stream_filter_remove(); // ??? is that right? 337 | switch ($this->encoding) { 338 | case Mime::ENCODING_QUOTEDPRINTABLE: 339 | if (array_key_exists(Mime::ENCODING_QUOTEDPRINTABLE, $this->filters)) { 340 | stream_filter_remove($this->filters[Mime::ENCODING_QUOTEDPRINTABLE]); 341 | } 342 | $filter = stream_filter_append( 343 | $this->content, 344 | 'convert.quoted-printable-encode', 345 | STREAM_FILTER_READ, 346 | [ 347 | 'line-length' => 76, 348 | 'line-break-chars' => $EOL 349 | ] 350 | ); 351 | $this->filters[Mime::ENCODING_QUOTEDPRINTABLE] = $filter; 352 | if (! is_resource($filter)) { 353 | throw new Exception\RuntimeException('Failed to append quoted-printable filter'); 354 | } 355 | break; 356 | case Mime::ENCODING_BASE64: 357 | if (array_key_exists(Mime::ENCODING_BASE64, $this->filters)) { 358 | stream_filter_remove($this->filters[Mime::ENCODING_BASE64]); 359 | } 360 | $filter = stream_filter_append( 361 | $this->content, 362 | 'convert.base64-encode', 363 | STREAM_FILTER_READ, 364 | [ 365 | 'line-length' => 76, 366 | 'line-break-chars' => $EOL 367 | ] 368 | ); 369 | $this->filters[Mime::ENCODING_BASE64] = $filter; 370 | if (! is_resource($filter)) { 371 | throw new Exception\RuntimeException('Failed to append base64 filter'); 372 | } 373 | break; 374 | default: 375 | } 376 | return $this->content; 377 | } 378 | 379 | /** 380 | * Get the Content of the current Mime Part in the given encoding. 381 | * 382 | * @param string $EOL 383 | * @return string 384 | */ 385 | public function getContent($EOL = Mime::LINEEND) 386 | { 387 | if ($this->isStream) { 388 | $encodedStream = $this->getEncodedStream($EOL); 389 | $encodedStreamContents = stream_get_contents($encodedStream); 390 | $streamMetaData = stream_get_meta_data($encodedStream); 391 | 392 | if (isset($streamMetaData['seekable']) && $streamMetaData['seekable']) { 393 | rewind($encodedStream); 394 | } 395 | 396 | return $encodedStreamContents; 397 | } 398 | return Mime::encode($this->content, $this->encoding, $EOL); 399 | } 400 | 401 | /** 402 | * Get the RAW unencoded content from this part 403 | * @return string 404 | */ 405 | public function getRawContent() 406 | { 407 | if ($this->isStream) { 408 | return stream_get_contents($this->content); 409 | } 410 | return $this->content; 411 | } 412 | 413 | /** 414 | * Create and return the array of headers for this MIME part 415 | * 416 | * @access public 417 | * @param string $EOL 418 | * @return array 419 | */ 420 | public function getHeadersArray($EOL = Mime::LINEEND) 421 | { 422 | $headers = []; 423 | 424 | $contentType = $this->type; 425 | if ($this->charset) { 426 | $contentType .= '; charset=' . $this->charset; 427 | } 428 | 429 | if ($this->boundary) { 430 | $contentType .= ';' . $EOL 431 | . " boundary=\"" . $this->boundary . '"'; 432 | } 433 | 434 | $headers[] = ['Content-Type', $contentType]; 435 | 436 | if ($this->encoding) { 437 | $headers[] = ['Content-Transfer-Encoding', $this->encoding]; 438 | } 439 | 440 | if ($this->id) { 441 | $headers[] = ['Content-ID', '<' . $this->id . '>']; 442 | } 443 | 444 | if ($this->disposition) { 445 | $disposition = $this->disposition; 446 | if ($this->filename) { 447 | $disposition .= '; filename="' . $this->filename . '"'; 448 | } 449 | $headers[] = ['Content-Disposition', $disposition]; 450 | } 451 | 452 | if ($this->description) { 453 | $headers[] = ['Content-Description', $this->description]; 454 | } 455 | 456 | if ($this->location) { 457 | $headers[] = ['Content-Location', $this->location]; 458 | } 459 | 460 | if ($this->language) { 461 | $headers[] = ['Content-Language', $this->language]; 462 | } 463 | 464 | return $headers; 465 | } 466 | 467 | /** 468 | * Return the headers for this part as a string 469 | * 470 | * @param string $EOL 471 | * @return String 472 | */ 473 | public function getHeaders($EOL = Mime::LINEEND) 474 | { 475 | $res = ''; 476 | foreach ($this->getHeadersArray($EOL) as $header) { 477 | $res .= $header[0] . ': ' . $header[1] . $EOL; 478 | } 479 | 480 | return $res; 481 | } 482 | } 483 | --------------------------------------------------------------------------------