From 06fe3944ed8e8bfac829b82e48f257d17f09e474 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 31 Aug 2012 14:44:59 -0500 Subject: [PATCH 1/2] [zendframework/zf2#2284][ZF2-507] Updated README - Notice about Date header --- .coveralls.yml | 3 + .gitattributes | 6 + .gitignore | 14 ++ .php_cs | 43 ++++ .travis.yml | 35 +++ CONTRIBUTING.md | 229 +++++++++++++++++ LICENSE.txt | 27 ++ README.md | 9 + composer.json | 39 +++ phpunit.xml.dist | 34 +++ phpunit.xml.travis | 34 +++ src/Decode.php | 224 +++++++++++++++++ src/Exception/ExceptionInterface.php | 19 ++ src/Exception/RuntimeException.php | 23 ++ src/Message.php | 266 ++++++++++++++++++++ src/Mime.php | 352 +++++++++++++++++++++++++++ src/Part.php | 211 ++++++++++++++++ test/MessageTest.php | 119 +++++++++ test/MimeTest.php | 157 ++++++++++++ test/PartTest.php | 110 +++++++++ test/bootstrap.php | 34 +++ 21 files changed, 1988 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 phpunit.xml.travis create mode 100644 src/Decode.php create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Message.php create mode 100644 src/Mime.php create mode 100644 src/Part.php create mode 100644 test/MessageTest.php create mode 100644 test/MimeTest.php create mode 100644 test/PartTest.php create mode 100644 test/bootstrap.php diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..53bda82 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +coverage_clover: clover.xml +json_path: coveralls-upload.json +src_dir: src diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..85dc9a8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/test export-ignore +/vendor export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +.php_cs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cac0a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.buildpath +.DS_Store +.idea +.project +.settings/ +.*.sw* +.*.un~ +nbproject +tmp/ + +clover.xml +coveralls-upload.json +phpunit.xml +vendor diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..bf4b799 --- /dev/null +++ b/.php_cs @@ -0,0 +1,43 @@ +notPath('TestAsset') + ->notPath('_files') + ->filter(function (SplFileInfo $file) { + if (strstr($file->getPath(), 'compatibility')) { + return false; + } + }); +$config = Symfony\CS\Config\Config::create(); +$config->level(null); +$config->fixers( + array( + 'braces', + 'duplicate_semicolon', + 'elseif', + 'empty_return', + 'encoding', + 'eof_ending', + 'function_call_space', + 'function_declaration', + 'indentation', + 'join_function', + 'line_after_namespace', + 'linefeed', + 'lowercase_keywords', + 'parenthesis', + 'multiple_use', + 'method_argument_space', + 'object_operator', + 'php_closing_tag', + 'psr0', + 'remove_lines_between_uses', + 'short_tag', + 'standardize_not_equal', + 'trailing_spaces', + 'unused_use', + 'visibility', + 'whitespacy_lines', + ) +); +$config->finder($finder); +return $config; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fe909ec --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +sudo: false + +language: php + +matrix: + fast_finish: true + include: + - php: 5.5 + - php: 5.6 + env: + - EXECUTE_TEST_COVERALLS=true + - EXECUTE_CS_CHECK=true + - php: 7 + - php: hhvm + allow_failures: + - php: 7 + - php: hhvm + +notifications: + irc: "irc.freenode.org#zftalk.dev" + email: false + +before_install: + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi + +install: + - composer install --no-interaction --prefer-source + +script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis --coverage-clover clover.xml ; fi + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis ; fi + - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run --config-file=.php_cs ; fi + +after_script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/coveralls ; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a64cada --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,229 @@ +# CONTRIBUTING + +## RESOURCES + +If you wish to contribute to Zend Framework, please be sure to +read/subscribe to the following resources: + + - [Coding Standards](https://github.com/zendframework/zf2/wiki/Coding-Standards) + - [Contributor's Guide](http://framework.zend.com/participate/contributor-guide) + - ZF Contributor's mailing list: + Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html + Subscribe: zf-contributors-subscribe@lists.zend.com + - ZF Contributor's IRC channel: + #zftalk.dev on Freenode.net + +If you are working on new features or refactoring [create a proposal](https://github.com/zendframework/zend-mime/issues/new). + +## Reporting Potential Security Issues + +If you have encountered a potential security vulnerability, please **DO NOT** report it on the public +issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead. +We will work with you to verify the vulnerability and patch it as soon as possible. + +When reporting issues, please provide the following information: + +- Component(s) affected +- A description indicating how to reproduce the issue +- A summary of the security vulnerability and impact + +We request that you contact us via the email address above and give the project +contributors a chance to resolve the vulnerability and issue a new release prior +to any public exposure; this helps protect users and provides them with a chance +to upgrade and/or update in order to protect their applications. + +For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc). + +## RUNNING TESTS + +> ### Note: testing versions prior to 2.4 +> +> This component originates with Zend Framework 2. During the lifetime of ZF2, +> testing infrastructure migrated from PHPUnit 3 to PHPUnit 4. In most cases, no +> changes were necessary. However, due to the migration, tests may not run on +> versions < 2.4. As such, you may need to change the PHPUnit dependency if +> attempting a fix on such a version. + +To run tests: + +- Clone the repository: + + ```console + $ git clone git@github.com:zendframework/zend-mime.git + $ cd + ``` + +- Install dependencies via composer: + + ```console + $ curl -sS https://getcomposer.org/installer | php -- + $ ./composer.phar install + ``` + + If you don't have `curl` installed, you can also download `composer.phar` from https://getcomposer.org/ + +- Run the tests via `phpunit` and the provided PHPUnit config, like in this example: + + ```console + $ ./vendor/bin/phpunit + ``` + +You can turn on conditional tests with the phpunit.xml file. +To do so: + + - Copy `phpunit.xml.dist` file to `phpunit.xml` + - Edit `phpunit.xml` to enable any specific functionality you + want to test, as well as to provide test values to utilize. + +## Running Coding Standards Checks + +This component uses [php-cs-fixer](http://cs.sensiolabs.org/) for coding +standards checks, and provides configuration for our selected checks. +`php-cs-fixer` is installed by default via Composer. + +To run checks only: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --dry-run --config-file=.php_cs +``` + +To have `php-cs-fixer` attempt to fix problems for you, omit the `--dry-run` +flag: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --config-file=.php_cs +``` + +If you allow php-cs-fixer to fix CS issues, please re-run the tests to ensure +they pass, and make sure you add and commit the changes after verification. + +## Recommended Workflow for Contributions + +Your first step is to establish a public repository from which we can +pull your work into the master repository. We recommend using +[GitHub](https://github.com), as that is where the component is already hosted. + +1. Setup a [GitHub account](http://github.com/), if you haven't yet +2. Fork the repository (http://github.com/zendframework/zend-mime) +3. Clone the canonical repository locally and enter it. + + ```console + $ git clone git://github.com:zendframework/zend-mime.git + $ cd zend-mime + ``` + +4. Add a remote to your fork; substitute your GitHub username in the command + below. + + ```console + $ git remote add {username} git@github.com:{username}/zend-mime.git + $ git fetch {username} + ``` + +### Keeping Up-to-Date + +Periodically, you should update your fork or personal repository to +match the canonical ZF repository. Assuming you have setup your local repository +per the instructions above, you can do the following: + + +```console +$ git checkout master +$ git fetch origin +$ git rebase origin/master +# OPTIONALLY, to keep your remote up-to-date - +$ git push {username} master:master +``` + +If you're tracking other branches -- for example, the "develop" branch, where +new feature development occurs -- you'll want to do the same operations for that +branch; simply substitute "develop" for "master". + +### Working on a patch + +We recommend you do each new feature or bugfix in a new branch. This simplifies +the task of code review as well as the task of merging your changes into the +canonical repository. + +A typical workflow will then consist of the following: + +1. Create a new local branch based off either your master or develop branch. +2. Switch to your new local branch. (This step can be combined with the + previous step with the use of `git checkout -b`.) +3. Do some work, commit, repeat as necessary. +4. Push the local branch to your remote repository. +5. Send a pull request. + +The mechanics of this process are actually quite trivial. Below, we will +create a branch for fixing an issue in the tracker. + +```console +$ git checkout -b hotfix/9295 +Switched to a new branch 'hotfix/9295' +``` + +... do some work ... + + +```console +$ git commit +``` + +... write your log message ... + + +```console +$ git push {username} hotfix/9295:hotfix/9295 +Counting objects: 38, done. +Delta compression using up to 2 threads. +Compression objects: 100% (18/18), done. +Writing objects: 100% (20/20), 8.19KiB, done. +Total 20 (delta 12), reused 0 (delta 0) +To ssh://git@github.com/{username}/zend-mime.git + b5583aa..4f51698 HEAD -> master +``` + +To send a pull request, you have two options. + +If using GitHub, you can do the pull request from there. Navigate to +your repository, select the branch you just created, and then select the +"Pull Request" button in the upper right. Select the user/organization +"zendframework" as the recipient. + +If using your own repository - or even if using GitHub - you can use `git +format-patch` to create a patchset for us to apply; in fact, this is +**recommended** for security-related patches. If you use `format-patch`, please +send the patches as attachments to: + +- zf-devteam@zend.com for patches without security implications +- zf-security@zend.com for security patches + +#### What branch to issue the pull request against? + +Which branch should you issue a pull request against? + +- For fixes against the stable release, issue the pull request against the + "master" branch. +- For new features, or fixes that introduce new elements to the public API (such + as new public methods or properties), issue the pull request against the + "develop" branch. + +### Branch Cleanup + +As you might imagine, if you are a frequent contributor, you'll start to +get a ton of branches both locally and on your remote. + +Once you know that your changes have been accepted to the master +repository, we suggest doing some cleanup of these branches. + +- Local branch cleanup + + ```console + $ git branch -d + ``` + +- Remote branch removal + + ```console + $ git push {username} : + ``` diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..6eab5aa --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2015, Zend Technologies USA, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Zend Technologies USA, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd2afa4 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# zend-mime + +`Zend\Mime` is a support class for handling multipart MIME messages. It is used +by `Zend\Mail` and `Zend\Mime\Message` and may be used by applications requiring +MIME support. + + +- File issues at https://github.com/zendframework/zend-mime/issues +- Documentation is at http://framework.zend.com/manual/current/en/index.html#zend-mime diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5a90f67 --- /dev/null +++ b/composer.json @@ -0,0 +1,39 @@ +{ + "name": "zendframework/zend-mime", + "description": " ", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "mime" + ], + "homepage": "https://github.com/zendframework/zend-mime", + "autoload": { + "psr-4": { + "Zend\\Mime": "src/" + } + }, + "require": { + "php": ">=5.3.3", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-mail": "self.version", + "fabpot/php-cs-fixer": "1.7.*", + "satooshi/php-coveralls": "dev-master", + "phpunit/PHPUnit": "~4.0" + }, + "suggest": { + "zendframework/zend-mail": "Zend\\Mail component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.4-dev", + "dev-develop": "2.5-dev" + } + }, + "autoload-dev": { + "psr-4": { + "ZendTest\\Mime\\": "test/" + } + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..3358a21 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,34 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + diff --git a/phpunit.xml.travis b/phpunit.xml.travis new file mode 100644 index 0000000..3358a21 --- /dev/null +++ b/phpunit.xml.travis @@ -0,0 +1,34 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + diff --git a/src/Decode.php b/src/Decode.php new file mode 100644 index 0000000..599dd87 --- /dev/null +++ b/src/Decode.php @@ -0,0 +1,224 @@ + array(name => value), 'body' => content), null if no parts found + * @throws Exception\RuntimeException + */ + public static function splitMessageStruct($message, $boundary, $EOL = Mime::LINEEND) + { + $parts = self::splitMime($message, $boundary); + if (count($parts) <= 0) { + return null; + } + $result = array(); + $headers = null; // "Declare" variable before the first usage "for reading" + $body = null; // "Declare" variable before the first usage "for reading" + foreach ($parts as $part) { + self::splitMessage($part, $headers, $body, $EOL); + $result[] = array('header' => $headers, + 'body' => $body ); + } + return $result; + } + + /** + * split a message in header and body part, if no header or an + * invalid header is found $headers is empty + * + * The charset of the returned headers depend on your iconv settings. + * + * @param string|Headers $message raw message with header and optional content + * @param Headers $headers output param, headers container + * @param string $body output param, content of message + * @param string $EOL EOL string; defaults to {@link Zend_Mime::LINEEND} + * @param boolean $strict enable strict mode for parsing message + * @return null + */ + public static function splitMessage($message, &$headers, &$body, $EOL = Mime::LINEEND, $strict = false) + { + if ($message instanceof Headers) { + $message = $message->toString(); + } + // check for valid header at first line + $firstline = strtok($message, "\n"); + if (!preg_match('%^[^\s]+[^:]*:%', $firstline)) { + $headers = array(); + // TODO: we're ignoring \r for now - is this function fast enough and is it safe to assume noone needs \r? + $body = str_replace(array("\r", "\n"), array('', $EOL), $message); + return; + } + + // see @ZF2-372, pops the first line off a message if it doesn't contain a header + if (!$strict) { + $parts = explode(': ', $firstline, 2); + if (count($parts) != 2) { + $message = substr($message, strpos($message, $EOL)+1); + } + } + + // find an empty line between headers and body + // default is set new line + if (strpos($message, $EOL . $EOL)) { + list($headers, $body) = explode($EOL . $EOL, $message, 2); + // next is the standard new line + } elseif ($EOL != "\r\n" && strpos($message, "\r\n\r\n")) { + list($headers, $body) = explode("\r\n\r\n", $message, 2); + // next is the other "standard" new line + } elseif ($EOL != "\n" && strpos($message, "\n\n")) { + list($headers, $body) = explode("\n\n", $message, 2); + // at last resort find anything that looks like a new line + } else { + ErrorHandler::start(E_NOTICE|E_WARNING); + list($headers, $body) = preg_split("%([\r\n]+)\\1%U", $message, 2); + ErrorHandler::stop(); + } + + $headers = Headers::fromString($headers, $EOL); + } + + /** + * split a content type in its different parts + * + * @param string $type content-type + * @param string $wantedPart the wanted part, else an array with all parts is returned + * @return string|array wanted part or all parts as array('type' => content-type, partname => value) + */ + public static function splitContentType($type, $wantedPart = null) + { + return self::splitHeaderField($type, $wantedPart, 'type'); + } + + /** + * split a header field like content type in its different parts + * + * @param string $field header field + * @param string $wantedPart the wanted part, else an array with all parts is returned + * @param string $firstName key name for the first part + * @return string|array wanted part or all parts as array($firstName => firstPart, partname => value) + * @throws Exception\RuntimeException + */ + public static function splitHeaderField($field, $wantedPart = null, $firstName = '0') + { + $wantedPart = strtolower($wantedPart); + $firstName = strtolower($firstName); + + // special case - a bit optimized + if ($firstName === $wantedPart) { + $field = strtok($field, ';'); + return $field[0] == '"' ? substr($field, 1, -1) : $field; + } + + $field = $firstName . '=' . $field; + if (!preg_match_all('%([^=\s]+)\s*=\s*("[^"]+"|[^;]+)(;\s*|$)%', $field, $matches)) { + throw new Exception\RuntimeException('not a valid header field'); + } + + if ($wantedPart) { + foreach ($matches[1] as $key => $name) { + if (strcasecmp($name, $wantedPart)) { + continue; + } + if ($matches[2][$key][0] != '"') { + return $matches[2][$key]; + } + return substr($matches[2][$key], 1, -1); + } + return null; + } + + $split = array(); + foreach ($matches[1] as $key => $name) { + $name = strtolower($name); + if ($matches[2][$key][0] == '"') { + $split[$name] = substr($matches[2][$key], 1, -1); + } else { + $split[$name] = $matches[2][$key]; + } + } + + return $split; + } + + /** + * decode a quoted printable encoded string + * + * The charset of the returned string depends on your iconv settings. + * + * @param string $string encoded string + * @return string decoded string + */ + public static function decodeQuotedPrintable($string) + { + return iconv_mime_decode($string, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8'); + } +} diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php new file mode 100644 index 0000000..6258d54 --- /dev/null +++ b/src/Exception/ExceptionInterface.php @@ -0,0 +1,19 @@ +parts; + } + + /** + * Sets the given array of Zend_Mime_Parts as the array for the message + * + * @param array $parts + */ + public function setParts($parts) + { + $this->parts = $parts; + } + + /** + * Append a new Zend_Mime_Part to the current message + * + * @param \Zend\Mime\Part $part + */ + public function addPart(Part $part) + { + /** + * @todo check for duplicate object handle + */ + $this->parts[] = $part; + } + + /** + * Check if message needs to be sent as multipart + * MIME message or if it has only one part. + * + * @return boolean + */ + public function isMultiPart() + { + return (count($this->parts) > 1); + } + + /** + * Set Zend_Mime object for the message + * + * This can be used to set the boundary specifically or to use a subclass of + * Zend_Mime for generating the boundary. + * + * @param \Zend\Mime\Mime $mime + */ + public function setMime(Mime $mime) + { + $this->mime = $mime; + } + + /** + * Returns the Zend_Mime object in use by the message + * + * If the object was not present, it is created and returned. Can be used to + * determine the boundary used in this message. + * + * @return \Zend\Mime\Mime + */ + public function getMime() + { + if ($this->mime === null) { + $this->mime = new Mime(); + } + + return $this->mime; + } + + /** + * Generate MIME-compliant message from the current configuration + * + * This can be a multipart message if more than one MIME part was added. If + * only one part is present, the content of this part is returned. If no + * part had been added, an empty string is returned. + * + * Parts are separated by the mime boundary as defined in Zend_Mime. If + * {@link setMime()} has been called before this method, the Zend_Mime + * object set by this call will be used. Otherwise, a new Zend_Mime object + * is generated and used. + * + * @param string $EOL EOL string; defaults to {@link Zend_Mime::LINEEND} + * @return string + */ + public function generateMessage($EOL = Mime::LINEEND) + { + if (! $this->isMultiPart()) { + $body = array_shift($this->parts); + $body = $body->getContent($EOL); + } else { + $mime = $this->getMime(); + + $boundaryLine = $mime->boundaryLine($EOL); + $body = 'This is a message in Mime Format. If you see this, ' + . "your mail reader does not support this format." . $EOL; + + foreach (array_keys($this->parts) as $p) { + $body .= $boundaryLine + . $this->getPartHeaders($p, $EOL) + . $EOL + . $this->getPartContent($p, $EOL); + } + + $body .= $mime->mimeEnd($EOL); + } + + return trim($body); + } + + /** + * Get the headers of a given part as an array + * + * @param int $partnum + * @return array + */ + public function getPartHeadersArray($partnum) + { + return $this->parts[$partnum]->getHeadersArray(); + } + + /** + * Get the headers of a given part as a string + * + * @param int $partnum + * @return string + */ + public function getPartHeaders($partnum, $EOL = Mime::LINEEND) + { + return $this->parts[$partnum]->getHeaders($EOL); + } + + /** + * Get the (encoded) content of a given part as a string + * + * @param int $partnum + * @return string + */ + public function getPartContent($partnum, $EOL = Mime::LINEEND) + { + return $this->parts[$partnum]->getContent($EOL); + } + + /** + * Explode MIME multipart string into separate parts + * + * Parts consist of the header and the body of each MIME part. + * + * @param string $body + * @param string $boundary + * @return array + */ + protected static function _disassembleMime($body, $boundary) + { + $start = 0; + $res = array(); + // find every mime part limiter and cut out the + // string before it. + // the part before the first boundary string is discarded: + $p = strpos($body, '--'.$boundary."\n", $start); + if ($p === false) { + // no parts found! + return array(); + } + + // position after first boundary line + $start = $p + 3 + strlen($boundary); + + while (($p = strpos($body, '--' . $boundary . "\n", $start)) !== false) { + $res[] = substr($body, $start, $p-$start); + $start = $p + 3 + strlen($boundary); + } + + // no more parts, find end boundary + $p = strpos($body, '--' . $boundary . '--', $start); + if ($p===false) { + throw new Exception\RuntimeException('Not a valid Mime Message: End Missing'); + } + + // the remaining part also needs to be parsed: + $res[] = substr($body, $start, $p-$start); + return $res; + } + + /** + * Decodes a MIME encoded string and returns a Zend_Mime_Message object with + * all the MIME parts set according to the given string + * + * @param string $message + * @param string $boundary + * @param string $EOL EOL string; defaults to {@link Zend_Mime::LINEEND} + * @return \Zend\Mime\Message + */ + public static function createFromMessage($message, $boundary, $EOL = Mime::LINEEND) + { + $parts = Decode::splitMessageStruct($message, $boundary, $EOL); + + $res = new self(); + foreach ($parts as $part) { + // now we build a new MimePart for the current Message Part: + $newPart = new Part($part['body']); + foreach ($part['header'] as $header) { + /** @var \Zend\Mail\Header\HeaderInterface $header */ + /** + * @todo check for characterset and filename + */ + + $fieldName = $header->getFieldName(); + $fieldValue = $header->getFieldValue(); + switch (strtolower($fieldName)) { + case 'content-type': + $newPart->type = $fieldValue; + break; + case 'content-transfer-encoding': + $newPart->encoding = $fieldValue; + break; + case 'content-id': + $newPart->id = trim($fieldValue,'<>'); + break; + case 'content-disposition': + $newPart->disposition = $fieldValue; + break; + case 'content-description': + $newPart->description = $fieldValue; + break; + case 'content-location': + $newPart->location = $fieldValue; + break; + case 'content-language': + $newPart->language = $fieldValue; + break; + default: + throw new Exception\RuntimeException('Unknown header ignored for MimePart:' . $fieldName); + } + } + $res->addPart($newPart); + } + return $res; + } +} diff --git a/src/Mime.php b/src/Mime.php new file mode 100644 index 0000000..1918902 --- /dev/null +++ b/src/Mime.php @@ -0,0 +1,352 @@ + $lineLength) { + $ptr = $lineLength; + } + + // Ensure we are not splitting across an encoded character + $pos = strrpos(substr($str, 0, $ptr), '='); + if ($pos !== false && $pos >= $ptr - 2) { + $ptr = $pos; + } + + // Check if there is a space at the end of the line and rewind + if ($ptr > 0 && $str[$ptr - 1] == ' ') { + --$ptr; + } + + // Add string and continue + $out .= substr($str, 0, $ptr) . '=' . $lineEnd; + $str = substr($str, $ptr); + } + + $out = rtrim($out, $lineEnd); + $out = rtrim($out, '='); + return $out; + } + + /** + * Converts a string into quoted printable format. + * + * @param string $str + * @return string + */ + private static function _encodeQuotedPrintable($str) + { + $str = str_replace('=', '=3D', $str); + $str = str_replace(self::$qpKeys, self::$qpReplaceValues, $str); + $str = rtrim($str); + return $str; + } + + /** + * Encode a given string with the QUOTED_PRINTABLE mechanism for Mail Headers. + * + * Mail headers depend on an extended quoted printable algorithm otherwise + * a range of bugs can occur. + * + * @param string $str + * @param string $charset + * @param int $lineLength Defaults to {@link LINELENGTH} + * @param int $lineEnd Defaults to {@link LINEEND} + * @return string + */ + public static function encodeQuotedPrintableHeader($str, $charset, + $lineLength = self::LINELENGTH, + $lineEnd = self::LINEEND) + { + // Reduce line-length by the length of the required delimiter, charsets and encoding + $prefix = sprintf('=?%s?Q?', $charset); + $lineLength = $lineLength-strlen($prefix)-3; + + $str = self::_encodeQuotedPrintable($str); + + // Mail-Header required chars have to be encoded also: + $str = str_replace(array('?', ' ', '_'), array('=3F', '=20', '=5F'), $str); + + // initialize first line, we need it anyways + $lines = array(0 => ""); + + // Split encoded text into separate lines + $tmp = ""; + while (strlen($str) > 0) { + $currentLine = max(count($lines)-1, 0); + $token = self::getNextQuotedPrintableToken($str); + $str = substr($str, strlen($token)); + + $tmp .= $token; + if ($token == '=20') { + // only if we have a single char token or space, we can append the + // tempstring it to the current line or start a new line if necessary. + if (strlen($lines[$currentLine].$tmp) > $lineLength) { + $lines[$currentLine+1] = $tmp; + } else { + $lines[$currentLine] .= $tmp; + } + $tmp = ""; + } + // don't forget to append the rest to the last line + if (strlen($str) == 0) { + $lines[$currentLine] .= $tmp; + } + } + + // assemble the lines together by pre- and appending delimiters, charset, encoding. + for ($i = 0; $i < count($lines); $i++) { + $lines[$i] = " ".$prefix.$lines[$i]."?="; + } + $str = trim(implode($lineEnd, $lines)); + return $str; + } + + /** + * Retrieves the first token from a quoted printable string. + * + * @param string $str + * @return string + */ + private static function getNextQuotedPrintableToken($str) + { + if (substr($str, 0, 1) == "=") { + $token = substr($str, 0, 3); + } else { + $token = substr($str, 0, 1); + } + return $token; + } + + /** + * Encode a given string in mail header compatible base64 encoding. + * + * @param string $str + * @param string $charset + * @param int $lineLength Defaults to {@link LINELENGTH} + * @param int $lineEnd Defaults to {@link LINEEND} + * @return string + */ + public static function encodeBase64Header($str, + $charset, + $lineLength = self::LINELENGTH, + $lineEnd = self::LINEEND) + { + $prefix = '=?' . $charset . '?B?'; + $suffix = '?='; + $remainingLength = $lineLength - strlen($prefix) - strlen($suffix); + + $encodedValue = self::encodeBase64($str, $remainingLength, $lineEnd); + $encodedValue = str_replace($lineEnd, $suffix . $lineEnd . ' ' . $prefix, $encodedValue); + $encodedValue = $prefix . $encodedValue . $suffix; + return $encodedValue; + } + + /** + * Encode a given string in base64 encoding and break lines + * according to the maximum linelength. + * + * @param string $str + * @param int $lineLength Defaults to {@link LINELENGTH} + * @param int $lineEnd Defaults to {@link LINEEND} + * @return string + */ + public static function encodeBase64($str, + $lineLength = self::LINELENGTH, + $lineEnd = self::LINEEND) + { + return rtrim(chunk_split(base64_encode($str), $lineLength, $lineEnd)); + } + + /** + * Constructor + * + * @param null|string $boundary + * @access public + */ + public function __construct($boundary = null) + { + // This string needs to be somewhat unique + if ($boundary === null) { + $this->boundary = '=_' . md5(microtime(1) . self::$makeUnique++); + } else { + $this->boundary = $boundary; + } + } + + /** + * Encode the given string with the given encoding. + * + * @param string $str + * @param string $encoding + * @param string $EOL EOL string; defaults to {@link Zend_Mime::LINEEND} + * @return string + */ + public static function encode($str, $encoding, $EOL = self::LINEEND) + { + switch ($encoding) { + case self::ENCODING_BASE64: + return self::encodeBase64($str, self::LINELENGTH, $EOL); + + case self::ENCODING_QUOTEDPRINTABLE: + return self::encodeQuotedPrintable($str, self::LINELENGTH, $EOL); + + default: + /** + * @todo 7Bit and 8Bit is currently handled the same way. + */ + return $str; + } + } + + /** + * Return a MIME boundary + * + * @access public + * @return string + */ + public function boundary() + { + return $this->boundary; + } + + /** + * Return a MIME boundary line + * + * @param mixed $EOL Defaults to {@link LINEEND} + * @access public + * @return string + */ + public function boundaryLine($EOL = self::LINEEND) + { + return $EOL . '--' . $this->boundary . $EOL; + } + + /** + * Return MIME ending + * + * @access public + * @return string + */ + public function mimeEnd($EOL = self::LINEEND) + { + return $EOL . '--' . $this->boundary . '--' . $EOL; + } +} diff --git a/src/Part.php b/src/Part.php new file mode 100644 index 0000000..c77ef85 --- /dev/null +++ b/src/Part.php @@ -0,0 +1,211 @@ +content = $content; + if (is_resource($content)) { + $this->isStream = true; + } + } + + /** + * @todo setters/getters + * @todo error checking for setting $type + * @todo error checking for setting $encoding + */ + + /** + * check if this part can be read as a stream. + * if true, getEncodedStream can be called, otherwise + * only getContent can be used to fetch the encoded + * content of the part + * + * @return bool + */ + public function isStream() + { + return $this->isStream; + } + + /** + * if this was created with a stream, return a filtered stream for + * reading the content. very useful for large file attachments. + * + * @return stream + * @throws Exception\RuntimeException if not a stream or unable to append filter + */ + public function getEncodedStream() + { + if (!$this->isStream) { + throw new Exception\RuntimeException('Attempt to get a stream from a string part'); + } + + //stream_filter_remove(); // ??? is that right? + switch ($this->encoding) { + case Mime::ENCODING_QUOTEDPRINTABLE: + $filter = stream_filter_append( + $this->content, + 'convert.quoted-printable-encode', + STREAM_FILTER_READ, + array( + 'line-length' => 76, + 'line-break-chars' => Mime::LINEEND + ) + ); + if (!is_resource($filter)) { + throw new Exception\RuntimeException('Failed to append quoted-printable filter'); + } + break; + case Mime::ENCODING_BASE64: + $filter = stream_filter_append( + $this->content, + 'convert.base64-encode', + STREAM_FILTER_READ, + array( + 'line-length' => 76, + 'line-break-chars' => Mime::LINEEND + ) + ); + if (!is_resource($filter)) { + throw new Exception\RuntimeException('Failed to append base64 filter'); + } + break; + default: + } + return $this->content; + } + + /** + * Get the Content of the current Mime Part in the given encoding. + * + * @return String + */ + public function getContent($EOL = Mime::LINEEND) + { + if ($this->isStream) { + return stream_get_contents($this->getEncodedStream()); + } else { + return Mime::encode($this->content, $this->encoding, $EOL); + } + } + + /** + * Get the RAW unencoded content from this part + * @return string + */ + public function getRawContent() + { + if ($this->isStream) { + return stream_get_contents($this->content); + } else { + return $this->content; + } + } + + /** + * Create and return the array of headers for this MIME part + * + * @access public + * @return array + */ + public function getHeadersArray($EOL = Mime::LINEEND) + { + $headers = array(); + + $contentType = $this->type; + if ($this->charset) { + $contentType .= '; charset=' . $this->charset; + } + + if ($this->boundary) { + $contentType .= ';' . $EOL + . " boundary=\"" . $this->boundary . '"'; + } + + $headers[] = array('Content-Type', $contentType); + + if ($this->encoding) { + $headers[] = array('Content-Transfer-Encoding', $this->encoding); + } + + if ($this->id) { + $headers[] = array('Content-ID', '<' . $this->id . '>'); + } + + if ($this->disposition) { + $disposition = $this->disposition; + if ($this->filename) { + $disposition .= '; filename="' . $this->filename . '"'; + } + $headers[] = array('Content-Disposition', $disposition); + } + + if ($this->description) { + $headers[] = array('Content-Description', $this->description); + } + + if ($this->location) { + $headers[] = array('Content-Location', $this->location); + } + + if ($this->language) { + $headers[] = array('Content-Language', $this->language); + } + + return $headers; + } + + /** + * Return the headers for this part as a string + * + * @return String + */ + public function getHeaders($EOL = Mime::LINEEND) + { + $res = ''; + foreach ($this->getHeadersArray($EOL) as $header) { + $res .= $header[0] . ': ' . $header[1] . $EOL; + } + + return $res; + } +} diff --git a/test/MessageTest.php b/test/MessageTest.php new file mode 100644 index 0000000..769834c --- /dev/null +++ b/test/MessageTest.php @@ -0,0 +1,119 @@ +assertFalse($msg->isMultiPart()); + } + + public function testSetGetParts() + { + $msg = new Mime\Message(); // No Parts + $p = $msg->getParts(); + $this->assertTrue(is_array($p)); + $this->assertTrue(count($p) == 0); + + $p2 = array(); + $p2[] = new Mime\Part('This is a test'); + $p2[] = new Mime\Part('This is another test'); + $msg->setParts($p2); + $p = $msg->getParts(); + $this->assertTrue(is_array($p)); + $this->assertTrue(count($p) == 2); + } + + public function testGetMime() + { + $msg = new Mime\Message(); // No Parts + $m = $msg->getMime(); + $this->assertInstanceOf('Zend\\Mime\\Mime', $m); + + $msg = new Mime\Message(); // No Parts + $mime = new Mime\Mime('1234'); + $msg->setMime($mime); + $m2 = $msg->getMime(); + $this->assertInstanceOf('Zend\\Mime\\Mime', $m2); + $this->assertEquals('1234', $m2->boundary()); + } + + public function testGenerate() + { + $msg = new Mime\Message(); // No Parts + $p1 = new Mime\Part('This is a test'); + $p2 = new Mime\Part('This is another test'); + $msg->addPart($p1); + $msg->addPart($p2); + $res = $msg->generateMessage(); + $mime = $msg->getMime(); + $boundary = $mime->boundary(); + $p1 = strpos($res, $boundary); + // $boundary must appear once for every mime part + $this->assertTrue($p1 !== false); + if ($p1) { + $p2 = strpos($res, $boundary, $p1 + strlen($boundary)); + $this->assertTrue($p2 !== false); + } + // check if the two test messages appear: + $this->assertTrue(strpos($res, 'This is a test') !== false); + $this->assertTrue(strpos($res, 'This is another test') !== false); + // ... more in ZMailTest + } + + /** + * check if decoding a string into a \Zend\Mime\Message object works + * + */ + public function testDecodeMimeMessage() + { + $text = << + +This is another test +--=_af4357ef34b786aae1491b0a2d14399f-- +EOD; + $res = Mime\Message::createFromMessage($text, '=_af4357ef34b786aae1491b0a2d14399f'); + + $parts = $res->getParts(); + $this->assertEquals(2, count($parts)); + + $part1 = $parts[0]; + $this->assertEquals('application/octet-stream', $part1->type); + $this->assertEquals('8bit', $part1->encoding); + + $part2 = $parts[1]; + $this->assertEquals('image/gif', $part2->type); + $this->assertEquals('base64', $part2->encoding); + $this->assertEquals('12', $part2->id); + } +} diff --git a/test/MimeTest.php b/test/MimeTest.php new file mode 100644 index 0000000..6360aaf --- /dev/null +++ b/test/MimeTest.php @@ -0,0 +1,157 @@ +_originaltimezone = date_default_timezone_get(); + } + + /** + * Tear down environment + */ + public function tearDown() + { + date_default_timezone_set($this->_originaltimezone); + } + + public function testBoundary() + { + // check boundary for uniqueness + $m1 = new Mime\Mime(); + $m2 = new Mime\Mime(); + $this->assertNotEquals($m1->boundary(), $m2->boundary()); + + // check instantiating with arbitrary boundary string + $myBoundary = 'mySpecificBoundary'; + $m3 = new Mime\Mime($myBoundary); + $this->assertEquals($m3->boundary(), $myBoundary); + + } + + public function testIsPrintable_notPrintable() + { + $this->assertFalse(Mime\Mime::isPrintable('Test with special chars: �����')); + } + + public function testIsPrintable_isPrintable() + { + $this->assertTrue(Mime\Mime::isPrintable('Test without special chars')); + } + + public function testQP() + { + $text = "This is a cool Test Text with special chars: ����\n" + . "and with multiple lines���� some of the Lines are long, long" + . ", long, long, long, long, long, long, long, long, long, long" + . ", long, long, long, long, long, long, long, long, long, long" + . ", long, long, long, long, long, long, long, long, long, long" + . ", long, long, long, long and with ����"; + + $qp = Mime\Mime::encodeQuotedPrintable($text); + $this->assertEquals(quoted_printable_decode($qp), $text); + } + + public function testBase64() + { + $content = str_repeat("\x88\xAA\xAF\xBF\x29\x88\xAA\xAF\xBF\x29\x88\xAA\xAF", 4); + $encoded = Mime\Mime::encodeBase64($content); + $this->assertEquals($content, base64_decode($encoded)); + } + + public function testZf1058WhitespaceAtEndOfBodyCausesInfiniteLoop() + { + $text = "my body\r\n\r\n...after two newlines\r\n "; + $result = quoted_printable_decode(Mime\Mime::encodeQuotedPrintable($text)); + $this->assertContains("my body\r\n\r\n...after two newlines", $result, $result); + } + + /** + * @group ZF-1688 + * @dataProvider dataTestEncodeMailHeaderQuotedPrintable + */ + public function testEncodeMailHeaderQuotedPrintable($str, $charset, $result) + { + $this->assertEquals($result, Mime\Mime::encodeQuotedPrintableHeader($str, $charset)); + } + + public static function dataTestEncodeMailHeaderQuotedPrintable() + { + return array( + array("äöü", "UTF-8", "=?UTF-8?Q?=C3=A4=C3=B6=C3=BC?="), + array("äöü ", "UTF-8", "=?UTF-8?Q?=C3=A4=C3=B6=C3=BC?="), + array("Gimme more €", "UTF-8", "=?UTF-8?Q?Gimme=20more=20=E2=82=AC?="), + array("Alle meine Entchen schwimmen in dem See, schwimmen in dem See, Köpfchen in das Wasser, Schwänzchen in die Höh!", "UTF-8", "=?UTF-8?Q?Alle=20meine=20Entchen=20schwimmen=20in=20dem=20See,=20?= + =?UTF-8?Q?schwimmen=20in=20dem=20See,=20K=C3=B6pfchen=20in=20das=20?= + =?UTF-8?Q?Wasser,=20Schw=C3=A4nzchen=20in=20die=20H=C3=B6h!?="), + array("ääääääääääääääääääääääääääääääääää", "UTF-8", "=?UTF-8?Q?=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4=C3=A4?="), + ); + } + + /** + * @group ZF-1688 + * @dataProvider dataTestEncodeMailHeaderBase64 + */ + public function testEncodeMailHeaderBase64($str, $charset, $result) + { + $this->assertEquals($result, Mime\Mime::encodeBase64Header($str, $charset)); + } + + public static function dataTestEncodeMailHeaderBase64() + { + return array( + array("äöü", "UTF-8", "=?UTF-8?B?w6TDtsO8?="), + array("Alle meine Entchen schwimmen in dem See, schwimmen in dem See, Köpfchen in das Wasser, Schwänzchen in die Höh!", "UTF-8", "=?UTF-8?B?QWxsZSBtZWluZSBFbnRjaGVuIHNjaHdpbW1lbiBpbiBkZW0gU2VlLCBzY2h3?= + =?UTF-8?B?aW1tZW4gaW4gZGVtIFNlZSwgS8O2cGZjaGVuIGluIGRhcyBXYXNzZXIsIFNj?= + =?UTF-8?B?aHfDpG56Y2hlbiBpbiBkaWUgSMO2aCE=?="), + ); + } + + /** + * @group ZF-1688 + */ + public function testLineLengthInQuotedPrintableHeaderEncoding() + { + $subject = "Alle meine Entchen schwimmen in dem See, schwimmen in dem See, Köpfchen in das Wasser, Schwänzchen in die Höh!"; + $encoded = Mime\Mime::encodeQuotedPrintableHeader($subject, "UTF-8", 100); + foreach (explode(Mime\Mime::LINEEND, $encoded) AS $line) { + if (strlen($line) > 100) { + $this->fail("Line '" . $line . "' is " . strlen($line) . " chars long, only 100 allowed."); + } + } + $encoded = Mime\Mime::encodeQuotedPrintableHeader($subject, "UTF-8", 40); + foreach (explode(Mime\Mime::LINEEND, $encoded) AS $line) { + if (strlen($line) > 40) { + $this->fail("Line '" . $line . "' is " . strlen($line) . " chars long, only 40 allowed."); + } + } + } +} diff --git a/test/PartTest.php b/test/PartTest.php new file mode 100644 index 0000000..cf6276e --- /dev/null +++ b/test/PartTest.php @@ -0,0 +1,110 @@ +testText = 'safdsafsa�lg ��gd�� sd�jg�sdjg�ld�gksd�gj�sdfg�dsj�gjsd�gj�dfsjg�dsfj�djs�g kjhdkj ' + . 'fgaskjfdh gksjhgjkdh gjhfsdghdhgksdjhg'; + $this->part = new Mime\Part($this->testText); + $this->part->encoding = Mime\Mime::ENCODING_BASE64; + $this->part->type = "text/plain"; + $this->part->filename = 'test.txt'; + $this->part->disposition = 'attachment'; + $this->part->charset = 'iso8859-1'; + $this->part->id = '4711'; + } + + public function testHeaders() + { + $expectedHeaders = array('Content-Type: text/plain', + 'Content-Transfer-Encoding: ' . Mime\Mime::ENCODING_BASE64, + 'Content-Disposition: attachment', + 'filename="test.txt"', + 'charset=iso8859-1', + 'Content-ID: <4711>'); + + $actual = $this->part->getHeaders(); + + foreach ($expectedHeaders as $expected) { + $this->assertContains($expected, $actual); + } + } + + public function testContentEncoding() + { + // Test with base64 encoding + $content = $this->part->getContent(); + $this->assertEquals($this->testText, base64_decode($content)); + // Test with quotedPrintable Encoding: + $this->part->encoding = Mime\Mime::ENCODING_QUOTEDPRINTABLE; + $content = $this->part->getContent(); + $this->assertEquals($this->testText, quoted_printable_decode($content)); + // Test with 8Bit encoding + $this->part->encoding = Mime\Mime::ENCODING_8BIT; + $content = $this->part->getContent(); + $this->assertEquals($this->testText, $content); + } + + public function testStreamEncoding() + { + $testfile = realpath(__FILE__); + $original = file_get_contents($testfile); + + // Test Base64 + $fp = fopen($testfile,'rb'); + $this->assertTrue(is_resource($fp)); + $part = new Mime\Part($fp); + $part->encoding = Mime\Mime::ENCODING_BASE64; + $fp2 = $part->getEncodedStream(); + $this->assertTrue(is_resource($fp2)); + $encoded = stream_get_contents($fp2); + fclose($fp); + $this->assertEquals(base64_decode($encoded),$original); + + // test QuotedPrintable + $fp = fopen($testfile,'rb'); + $this->assertTrue(is_resource($fp)); + $part = new Mime\Part($fp); + $part->encoding = Mime\Mime::ENCODING_QUOTEDPRINTABLE; + $fp2 = $part->getEncodedStream(); + $this->assertTrue(is_resource($fp2)); + $encoded = stream_get_contents($fp2); + fclose($fp); + $this->assertEquals(quoted_printable_decode($encoded),$original); + } + + /** + * @group ZF-1491 + */ + public function testGetRawContentFromPart() + { + $this->assertEquals($this->testText, $this->part->getRawContent()); + } +} diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 0000000..b09976b --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,34 @@ + Date: Sat, 1 Sep 2012 20:40:03 +0200 Subject: [PATCH 2/2] Resolve more mismatched phpDoc --- src/Mime.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Mime.php b/src/Mime.php index 1918902..d03b530 100644 --- a/src/Mime.php +++ b/src/Mime.php @@ -107,7 +107,7 @@ public static function isPrintable($str) * * @param string $str * @param int $lineLength Defaults to {@link LINELENGTH} - * @param int $lineEnd Defaults to {@link LINEEND} + * @param string $lineEnd Defaults to {@link LINEEND} * @return string */ public static function encodeQuotedPrintable($str, @@ -168,7 +168,7 @@ private static function _encodeQuotedPrintable($str) * @param string $str * @param string $charset * @param int $lineLength Defaults to {@link LINELENGTH} - * @param int $lineEnd Defaults to {@link LINEEND} + * @param string $lineEnd Defaults to {@link LINEEND} * @return string */ public static function encodeQuotedPrintableHeader($str, $charset, @@ -241,7 +241,7 @@ private static function getNextQuotedPrintableToken($str) * @param string $str * @param string $charset * @param int $lineLength Defaults to {@link LINELENGTH} - * @param int $lineEnd Defaults to {@link LINEEND} + * @param string $lineEnd Defaults to {@link LINEEND} * @return string */ public static function encodeBase64Header($str, @@ -265,7 +265,7 @@ public static function encodeBase64Header($str, * * @param string $str * @param int $lineLength Defaults to {@link LINELENGTH} - * @param int $lineEnd Defaults to {@link LINEEND} + * @param string $lineEnd Defaults to {@link LINEEND} * @return string */ public static function encodeBase64($str, @@ -330,7 +330,7 @@ public function boundary() /** * Return a MIME boundary line * - * @param mixed $EOL Defaults to {@link LINEEND} + * @param string $EOL Defaults to {@link LINEEND} * @access public * @return string */ @@ -342,6 +342,7 @@ public function boundaryLine($EOL = self::LINEEND) /** * Return MIME ending * + * @param string $EOL Defaults to {@link LINEEND} * @access public * @return string */