diff --git a/README.md b/README.md index 2470f4f1..f1530872 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # zbateson/mail-mime-parser -Standalone, testable and PSR-compliant mail mime parser alternative to PHP's imap* functions and Pear libraries for reading messages in _Internet Message Format_ ([RFC 5322](http://tools.ietf.org/html/rfc5322), [RFC 2822](http://tools.ietf.org/html/rfc2822) and [RFC 822](http://tools.ietf.org/html/rfc822)). +Standalone, testable and PSR-compliant mail mime parser alternative to PHP's imap* functions and Pear libraries for reading messages in _Internet Message Format_ [RFC 822](http://tools.ietf.org/html/rfc822) (and later revisions [RFC 2822](http://tools.ietf.org/html/rfc2822), [RFC 5322](http://tools.ietf.org/html/rfc5322)). [![Build Status](https://travis-ci.org/zbateson/MailMimeParser.svg?branch=master)](https://travis-ci.org/zbateson/MailMimeParser) [![Code Coverage](https://scrutinizer-ci.com/g/zbateson/MailMimeParser/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/zbateson/MailMimeParser/?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/zbateson/MailMimeParser/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/zbateson/MailMimeParser/?branch=master) [![Total Downloads](https://poser.pugx.org/zbateson/mail-mime-parser/downloads)](https://packagist.org/packages/zbateson/mail-mime-parser) diff --git a/composer.json b/composer.json index aba3285d..17b65473 100644 --- a/composer.json +++ b/composer.json @@ -12,13 +12,16 @@ ], "require": { "php": ">=5.4", - "ext-mbstring": "*" + "ext-mbstring": "*", + "guzzlehttp/psr7": "^1.0.0", + "zbateson/stream-decorators": "^0.4.0" }, "require-dev": { "phpunit/phpunit": "~4.5.0", "phing/phing": "~2.15.0", "evert/phpdoc-md": "~0.1.1", - "phpdocumentor/phpdocumentor": "~2.8.0" + "phpdocumentor/phpdocumentor": "~2.8.0", + "mikey179/vfsStream": "~1.6.0" }, "autoload": { "psr-4": {"ZBateson\\MailMimeParser\\": "src/"} diff --git a/composer.lock b/composer.lock index e93d246f..69166e97 100644 --- a/composer.lock +++ b/composer.lock @@ -1,12 +1,176 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "hash": "6806d335bf9d76dc1359ae7b2dd46c69", - "content-hash": "abbf7a66974e6ea35082997b8574b4ff", - "packages": [], + "content-hash": "f8e3f2e19dd40142c119bbfbc1ba06e7", + "packages": [ + { + "name": "guzzlehttp/psr7", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2017-03-20T17:10:46+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "zbateson/stream-decorators", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/zbateson/StreamDecorators.git", + "reference": "a4499c1bd1b4ed8d19bcac9c9304cf2ead115ec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zbateson/StreamDecorators/zipball/a4499c1bd1b4ed8d19bcac9c9304cf2ead115ec5", + "reference": "a4499c1bd1b4ed8d19bcac9c9304cf2ead115ec5", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "guzzlehttp/psr7": "^1.0.0", + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "~4.5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZBateson\\StreamDecorators\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Zaahid Bateson", + "email": "zbateson@users.noreply.github.com" + } + ], + "description": "PHP psr7 stream decorators for mime message part streams", + "homepage": "https://github.com/zbateson/StreamDecorators", + "keywords": [ + "decorators", + "mail", + "mime", + "psr7", + "stream" + ], + "time": "2018-07-22T19:34:40+00:00" + } + ], "packages-dev": [ { "name": "cilex/cilex", @@ -65,7 +229,7 @@ "cli", "microframework" ], - "time": "2014-03-29 14:03:13" + "time": "2014-03-29T14:03:13+00:00" }, { "name": "cilex/console-service-provider", @@ -124,7 +288,7 @@ "service-provider", "silex" ], - "time": "2012-12-19 10:50:58" + "time": "2012-12-19T10:50:58+00:00" }, { "name": "container-interop/container-interop", @@ -155,34 +319,34 @@ ], "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", "homepage": "https://github.com/container-interop/container-interop", - "time": "2017-02-14 19:40:03" + "time": "2017-02-14T19:40:03+00:00" }, { "name": "doctrine/annotations", - "version": "v1.4.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "54cacc9b81758b14e3ce750f205a393d52339e97" + "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97", - "reference": "54cacc9b81758b14e3ce750f205a393d52339e97", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", + "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", "shasum": "" }, "require": { "doctrine/lexer": "1.*", - "php": "^5.6 || ^7.0" + "php": "^7.1" }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "1.6.x-dev" } }, "autoload": { @@ -223,36 +387,36 @@ "docblock", "parser" ], - "time": "2017-02-24 16:22:25" + "time": "2017-12-06T07:11:42+00:00" }, { "name": "doctrine/instantiator", - "version": "1.0.5", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": "^7.1" }, "require-dev": { "athletic/athletic": "~0.1.8", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "phpunit/phpunit": "^6.2.3", + "squizlabs/php_codesniffer": "^3.0.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -277,7 +441,7 @@ "constructor", "instantiate" ], - "time": "2015-06-14 21:17:01" + "time": "2017-07-22T11:58:36+00:00" }, { "name": "doctrine/lexer", @@ -331,25 +495,29 @@ "lexer", "parser" ], - "time": "2014-09-09 13:34:57" + "time": "2014-09-09T13:34:57+00:00" }, { "name": "erusev/parsedown", - "version": "1.6.3", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/erusev/parsedown.git", - "reference": "728952b90a333b5c6f77f06ea9422b94b585878d" + "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/728952b90a333b5c6f77f06ea9422b94b585878d", - "reference": "728952b90a333b5c6f77f06ea9422b94b585878d", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/92e9c27ba0e74b8b028b111d1b6f956a15c01fc1", + "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1", "shasum": "" }, "require": { + "ext-mbstring": "*", "php": ">=5.3.0" }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, "type": "library", "autoload": { "psr-0": { @@ -373,7 +541,7 @@ "markdown", "parser" ], - "time": "2017-05-14 14:47:48" + "time": "2018-03-08T01:11:30+00:00" }, { "name": "evert/phpdoc-md", @@ -424,7 +592,7 @@ "php", "phpdoc" ], - "time": "2015-03-26 23:24:48" + "time": "2015-03-26T23:24:48+00:00" }, { "name": "herrera-io/json", @@ -485,7 +653,7 @@ "validate" ], "abandoned": "kherge/json", - "time": "2013-10-30 16:51:34" + "time": "2013-10-30T16:51:34+00:00" }, { "name": "herrera-io/phar-update", @@ -543,7 +711,7 @@ "update" ], "abandoned": true, - "time": "2013-10-30 17:23:01" + "time": "2013-10-30T17:23:01+00:00" }, { "name": "jms/metadata", @@ -594,7 +762,7 @@ "xml", "yaml" ], - "time": "2016-12-05 10:18:33" + "time": "2016-12-05T10:18:33+00:00" }, { "name": "jms/parser-lib", @@ -629,7 +797,7 @@ "Apache2" ], "description": "A library for easily creating recursive-descent parsers.", - "time": "2012-11-18 18:08:43" + "time": "2012-11-18T18:08:43+00:00" }, { "name": "jms/serializer", @@ -699,7 +867,7 @@ "serialization", "xml" ], - "time": "2014-03-18 08:39:00" + "time": "2014-03-18T08:39:00+00:00" }, { "name": "justinrainbow/json-schema", @@ -765,7 +933,7 @@ "json", "schema" ], - "time": "2016-01-25 15:43:01" + "time": "2016-01-25T15:43:01+00:00" }, { "name": "kherge/version", @@ -808,7 +976,53 @@ "description": "A parsing and comparison library for semantic versioning.", "homepage": "http://github.com/kherge/Version", "abandoned": true, - "time": "2012-08-16 17:13:03" + "time": "2012-08-16T17:13:03+00:00" + }, + { + "name": "mikey179/vfsStream", + "version": "v1.6.5", + "source": { + "type": "git", + "url": "https://github.com/mikey179/vfsStream.git", + "reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikey179/vfsStream/zipball/d5fec95f541d4d71c4823bb5e30cf9b9e5b96145", + "reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "time": "2017-08-01T08:02:14+00:00" }, { "name": "monolog/monolog", @@ -886,7 +1100,7 @@ "logging", "psr-3" ], - "time": "2017-06-19 01:22:40" + "time": "2017-06-19T01:22:40+00:00" }, { "name": "nikic/php-parser", @@ -931,7 +1145,7 @@ "parser", "php" ], - "time": "2014-07-23 18:24:17" + "time": "2014-07-23T18:24:17+00:00" }, { "name": "phing/phing", @@ -1024,7 +1238,7 @@ "task", "tool" ], - "time": "2016-10-13 09:01:45" + "time": "2016-10-13T09:01:45+00:00" }, { "name": "phpcollection/phpcollection", @@ -1072,7 +1286,7 @@ "sequence", "set" ], - "time": "2015-05-17 12:39:23" + "time": "2015-05-17T12:39:23+00:00" }, { "name": "phpdocumentor/fileset", @@ -1115,7 +1329,7 @@ "fileset", "phpdoc" ], - "time": "2013-08-06 21:07:42" + "time": "2013-08-06T21:07:42+00:00" }, { "name": "phpdocumentor/graphviz", @@ -1156,7 +1370,7 @@ "email": "mike.vanriel@naenius.com" } ], - "time": "2016-02-02 13:00:08" + "time": "2016-02-02T13:00:08+00:00" }, { "name": "phpdocumentor/phpdocumentor", @@ -1245,7 +1459,7 @@ "documentation", "phpdoc" ], - "time": "2015-07-28 06:36:40" + "time": "2015-07-28T06:36:40+00:00" }, { "name": "phpdocumentor/reflection", @@ -1299,7 +1513,7 @@ "reflection", "static analysis" ], - "time": "2014-11-14 11:43:04" + "time": "2014-11-14T11:43:04+00:00" }, { "name": "phpdocumentor/reflection-docblock", @@ -1348,7 +1562,7 @@ "email": "mike.vanriel@naenius.com" } ], - "time": "2016-01-25 08:17:30" + "time": "2016-01-25T08:17:30+00:00" }, { "name": "phpoption/phpoption", @@ -1398,37 +1612,37 @@ "php", "type" ], - "time": "2015-07-25 16:39:46" + "time": "2015-07-25T16:39:46+00:00" }, { "name": "phpspec/prophecy", - "version": "v1.7.0", + "version": "1.7.6", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" + "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", - "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712", + "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", - "sebastian/comparator": "^1.1|^2.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "sebastian/comparator": "^1.1|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8 || ^5.6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.7.x-dev" } }, "autoload": { @@ -1461,7 +1675,7 @@ "spy", "stub" ], - "time": "2017-03-02 20:05:34" + "time": "2018-04-18T13:57:24+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1523,7 +1737,7 @@ "testing", "xunit" ], - "time": "2015-10-06 15:47:00" + "time": "2015-10-06T15:47:00+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1568,7 +1782,7 @@ "filesystem", "iterator" ], - "time": "2013-10-10 15:34:57" + "time": "2013-10-10T15:34:57+00:00" }, { "name": "phpunit/php-text-template", @@ -1609,7 +1823,7 @@ "keywords": [ "template" ], - "time": "2015-06-21 13:50:34" + "time": "2015-06-21T13:50:34+00:00" }, { "name": "phpunit/php-timer", @@ -1658,20 +1872,20 @@ "keywords": [ "timer" ], - "time": "2017-02-26 11:10:40" + "time": "2017-02-26T11:10:40+00:00" }, { "name": "phpunit/php-token-stream", - "version": "1.4.11", + "version": "1.4.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7" + "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16", + "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16", "shasum": "" }, "require": { @@ -1707,7 +1921,7 @@ "keywords": [ "tokenizer" ], - "time": "2017-02-27 10:12:30" + "time": "2017-12-04T08:55:13+00:00" }, { "name": "phpunit/phpunit", @@ -1779,7 +1993,7 @@ "testing", "xunit" ], - "time": "2015-03-29 09:24:05" + "time": "2015-03-29T09:24:05+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -1835,7 +2049,7 @@ "mock", "xunit" ], - "time": "2015-10-02 06:51:40" + "time": "2015-10-02T06:51:40+00:00" }, { "name": "pimple/pimple", @@ -1881,7 +2095,53 @@ "container", "dependency injection" ], - "time": "2013-11-22 08:30:29" + "time": "2013-11-22T08:30:29+00:00" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" }, { "name": "psr/container", @@ -1930,7 +2190,7 @@ "container-interop", "psr" ], - "time": "2017-02-14 16:28:37" + "time": "2017-02-14T16:28:37+00:00" }, { "name": "psr/log", @@ -1977,7 +2237,55 @@ "psr", "psr-3" ], - "time": "2016-10-10 12:19:37" + "time": "2016-10-10T12:19:37+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" }, { "name": "sebastian/comparator", @@ -2041,7 +2349,7 @@ "compare", "equality" ], - "time": "2017-01-29 09:50:25" + "time": "2017-01-29T09:50:25+00:00" }, { "name": "sebastian/diff", @@ -2093,7 +2401,7 @@ "keywords": [ "diff" ], - "time": "2017-05-22 07:24:03" + "time": "2017-05-22T07:24:03+00:00" }, { "name": "sebastian/environment", @@ -2143,7 +2451,7 @@ "environment", "hhvm" ], - "time": "2016-08-18 05:49:44" + "time": "2016-08-18T05:49:44+00:00" }, { "name": "sebastian/exporter", @@ -2210,7 +2518,7 @@ "export", "exporter" ], - "time": "2016-06-17 09:04:28" + "time": "2016-06-17T09:04:28+00:00" }, { "name": "sebastian/global-state", @@ -2261,7 +2569,7 @@ "keywords": [ "global state" ], - "time": "2015-10-12 03:26:01" + "time": "2015-10-12T03:26:01+00:00" }, { "name": "sebastian/recursion-context", @@ -2314,7 +2622,7 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-10-03 07:41:43" + "time": "2016-10-03T07:41:43+00:00" }, { "name": "sebastian/version", @@ -2349,27 +2657,27 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-06-21 13:59:46" + "time": "2015-06-21T13:59:46+00:00" }, { "name": "seld/jsonlint", - "version": "1.6.1", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "50d63f2858d87c4738d5b76a7dcbb99fa8cf7c77" + "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/50d63f2858d87c4738d5b76a7dcbb99fa8cf7c77", - "reference": "50d63f2858d87c4738d5b76a7dcbb99fa8cf7c77", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/d15f59a67ff805a44c50ea0516d2341740f81a38", + "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38", "shasum": "" }, "require": { "php": "^5.3 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" }, "bin": [ "bin/jsonlint" @@ -2398,25 +2706,26 @@ "parser", "validator" ], - "time": "2017-06-18 15:11:04" + "time": "2018-01-24T12:46:19+00:00" }, { "name": "symfony/config", - "version": "v2.8.24", + "version": "v2.8.42", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "0b8541d18507d10204a08384640ff6df3c739ebe" + "reference": "93bdf96d0e3c9b29740bf9050e7a996b443c8436" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/0b8541d18507d10204a08384640ff6df3c739ebe", - "reference": "0b8541d18507d10204a08384640ff6df3c739ebe", + "url": "https://api.github.com/repos/symfony/config/zipball/93bdf96d0e3c9b29740bf9050e7a996b443c8436", + "reference": "93bdf96d0e3c9b29740bf9050e7a996b443c8436", "shasum": "" }, "require": { "php": ">=5.3.9", - "symfony/filesystem": "~2.3|~3.0.0" + "symfony/filesystem": "~2.3|~3.0.0", + "symfony/polyfill-ctype": "~1.8" }, "require-dev": { "symfony/yaml": "~2.7|~3.0.0" @@ -2454,20 +2763,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-04-12 14:07:15" + "time": "2018-05-01T22:52:40+00:00" }, { "name": "symfony/console", - "version": "v2.8.24", + "version": "v2.8.42", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "46e65f8d98c9ab629bbfcc16a4ff023f61c37fb2" + "reference": "e8e59b74ad1274714dad2748349b55e3e6e630c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/46e65f8d98c9ab629bbfcc16a4ff023f61c37fb2", - "reference": "46e65f8d98c9ab629bbfcc16a4ff023f61c37fb2", + "url": "https://api.github.com/repos/symfony/console/zipball/e8e59b74ad1274714dad2748349b55e3e6e630c7", + "reference": "e8e59b74ad1274714dad2748349b55e3e6e630c7", "shasum": "" }, "require": { @@ -2481,7 +2790,7 @@ "symfony/process": "~2.1|~3.0.0" }, "suggest": { - "psr/log": "For using the console logger", + "psr/log-implementation": "For using the console logger", "symfony/event-dispatcher": "", "symfony/process": "" }, @@ -2515,7 +2824,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-07-03 08:04:30" + "time": "2018-05-15T21:17:45+00:00" }, { "name": "symfony/debug", @@ -2572,20 +2881,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2016-07-30 07:22:48" + "time": "2016-07-30T07:22:48+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.24", + "version": "v2.8.42", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "1377400fd641d7d1935981546aaef780ecd5bf6d" + "reference": "9b69aad7d4c086dc94ebade2d5eb9145da5dac8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/1377400fd641d7d1935981546aaef780ecd5bf6d", - "reference": "1377400fd641d7d1935981546aaef780ecd5bf6d", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9b69aad7d4c086dc94ebade2d5eb9145da5dac8c", + "reference": "9b69aad7d4c086dc94ebade2d5eb9145da5dac8c", "shasum": "" }, "require": { @@ -2632,7 +2941,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-06-02 07:47:27" + "time": "2018-04-06T07:35:03+00:00" }, { "name": "symfony/filesystem", @@ -2681,20 +2990,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2016-07-20 05:43:46" + "time": "2016-07-20T05:43:46+00:00" }, { "name": "symfony/finder", - "version": "v2.8.24", + "version": "v2.8.42", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "4f4e84811004e065a3bb5ceeb1d9aa592630f9ad" + "reference": "995cd7c28a0778cece02e2133b4d813dc509dfc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/4f4e84811004e065a3bb5ceeb1d9aa592630f9ad", - "reference": "4f4e84811004e065a3bb5ceeb1d9aa592630f9ad", + "url": "https://api.github.com/repos/symfony/finder/zipball/995cd7c28a0778cece02e2133b4d813dc509dfc3", + "reference": "995cd7c28a0778cece02e2133b4d813dc509dfc3", "shasum": "" }, "require": { @@ -2730,20 +3039,75 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-06-01 20:52:29" + "time": "2018-06-19T11:07:17+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae", + "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-04-30T19:57:29+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.4.0", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "f29dca382a6485c3cbe6379f0c61230167681937" + "reference": "3296adf6a6454a050679cde90f95350ad604b171" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937", - "reference": "f29dca382a6485c3cbe6379f0c61230167681937", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", + "reference": "3296adf6a6454a050679cde90f95350ad604b171", "shasum": "" }, "require": { @@ -2755,7 +3119,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.8-dev" } }, "autoload": { @@ -2789,20 +3153,20 @@ "portable", "shim" ], - "time": "2017-06-09 14:24:12" + "time": "2018-04-26T10:06:28+00:00" }, { "name": "symfony/process", - "version": "v2.8.24", + "version": "v2.8.42", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "57e52a0a6a80ea0aec4fc1b785a7920a95cb88a8" + "reference": "542d88b350c42750fdc14e73860ee96dd423e95d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/57e52a0a6a80ea0aec4fc1b785a7920a95cb88a8", - "reference": "57e52a0a6a80ea0aec4fc1b785a7920a95cb88a8", + "url": "https://api.github.com/repos/symfony/process/zipball/542d88b350c42750fdc14e73860ee96dd423e95d", + "reference": "542d88b350c42750fdc14e73860ee96dd423e95d", "shasum": "" }, "require": { @@ -2838,20 +3202,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-07-03 08:04:30" + "time": "2018-05-27T07:40:52+00:00" }, { "name": "symfony/stopwatch", - "version": "v2.8.24", + "version": "v2.8.42", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "e02577b841394a78306d7b547701bb7bb705bad5" + "reference": "57021208ad9830f8f8390c1a9d7bb390f32be89e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/e02577b841394a78306d7b547701bb7bb705bad5", - "reference": "e02577b841394a78306d7b547701bb7bb705bad5", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/57021208ad9830f8f8390c1a9d7bb390f32be89e", + "reference": "57021208ad9830f8f8390c1a9d7bb390f32be89e", "shasum": "" }, "require": { @@ -2887,7 +3251,7 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2017-04-12 14:07:15" + "time": "2018-01-03T07:36:31+00:00" }, { "name": "symfony/translation", @@ -2951,24 +3315,25 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2016-07-30 07:22:48" + "time": "2016-07-30T07:22:48+00:00" }, { "name": "symfony/validator", - "version": "v2.8.24", + "version": "v2.8.42", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "6c019627f2a69b9ab2ac41fd53102148a55af564" + "reference": "3fa2355675f1ebc074589b83478480745342c970" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/6c019627f2a69b9ab2ac41fd53102148a55af564", - "reference": "6c019627f2a69b9ab2ac41fd53102148a55af564", + "url": "https://api.github.com/repos/symfony/validator/zipball/3fa2355675f1ebc074589b83478480745342c970", + "reference": "3fa2355675f1ebc074589b83478480745342c970", "shasum": "" }, "require": { "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", "symfony/translation": "~2.4|~3.0.0" }, @@ -3024,24 +3389,25 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2017-07-03 08:04:30" + "time": "2018-06-19T08:02:14+00:00" }, { "name": "symfony/yaml", - "version": "v2.8.24", + "version": "v2.8.42", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "4c29dec8d489c4e37cf87ccd7166cd0b0e6a45c5" + "reference": "51356b7a2ff7c9fd06b2f1681cc463bb62b5c1ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/4c29dec8d489c4e37cf87ccd7166cd0b0e6a45c5", - "reference": "4c29dec8d489c4e37cf87ccd7166cd0b0e6a45c5", + "url": "https://api.github.com/repos/symfony/yaml/zipball/51356b7a2ff7c9fd06b2f1681cc463bb62b5c1ff", + "reference": "51356b7a2ff7c9fd06b2f1681cc463bb62b5c1ff", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { @@ -3073,7 +3439,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-06-01 20:52:29" + "time": "2018-05-01T22:52:40+00:00" }, { "name": "twig/twig", @@ -3130,34 +3496,41 @@ "keywords": [ "templating" ], - "time": "2015-06-06 23:31:24" + "time": "2015-06-06T23:31:24+00:00" }, { "name": "zendframework/zend-cache", - "version": "2.7.2", + "version": "2.8.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-cache.git", - "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039" + "reference": "4983dff629956490c78b88adcc8ece4711d7d8a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/c98331b96d3b9d9b24cf32d02660602edb34d039", - "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039", + "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/4983dff629956490c78b88adcc8ece4711d7d8a3", + "reference": "4983dff629956490c78b88adcc8ece4711d7d8a3", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", - "zendframework/zend-stdlib": "^2.7 || ^3.0" + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "zendframework/zend-eventmanager": "^2.6.3 || ^3.2", + "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", + "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + }, + "provide": { + "psr/cache-implementation": "1.0", + "psr/simple-cache-implementation": "1.0" }, "require-dev": { - "phpbench/phpbench": "^0.10.0", - "phpunit/phpunit": "^4.8", + "cache/integration-tests": "^0.16", + "phpbench/phpbench": "^0.13", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-serializer": "^2.6", - "zendframework/zend-session": "^2.6.2" + "zendframework/zend-session": "^2.7.4" }, "suggest": { "ext-apc": "APC or compatible extension, to use the APC storage adapter", @@ -3166,9 +3539,11 @@ "ext-memcache": "Memcache >= 2.0.0 to use the Memcache storage adapter", "ext-memcached": "Memcached >= 1.0.0 to use the Memcached storage adapter", "ext-mongo": "Mongo, to use MongoDb storage adapter", + "ext-mongodb": "MongoDB, to use the ExtMongoDb storage adapter", "ext-redis": "Redis, to use Redis storage adapter", "ext-wincache": "WinCache, to use the WinCache storage adapter", "ext-xcache": "XCache, to use the XCache storage adapter", + "mongodb/mongodb": "Required for use with the ext-mongodb adapter", "mongofill/mongofill": "Alternative to ext-mongo - a pure PHP implementation designed as a drop in replacement", "zendframework/zend-serializer": "Zend\\Serializer component", "zendframework/zend-session": "Zend\\Session component" @@ -3176,8 +3551,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" + "dev-master": "2.8.x-dev", + "dev-develop": "2.9.x-dev" }, "zf": { "component": "Zend\\Cache", @@ -3185,6 +3560,9 @@ } }, "autoload": { + "files": [ + "autoload/patternPluginManagerPolyfill.php" + ], "psr-4": { "Zend\\Cache\\": "src/" } @@ -3193,13 +3571,15 @@ "license": [ "BSD-3-Clause" ], - "description": "provides a generic way to cache any data", - "homepage": "https://github.com/zendframework/zend-cache", + "description": "Caching implementation with a variety of storage options, as well as codified caching strategies for callbacks, classes, and output", "keywords": [ + "ZendFramework", "cache", - "zf2" + "psr-16", + "psr-6", + "zf" ], - "time": "2016-12-16 11:35:47" + "time": "2018-05-01T21:58:00+00:00" }, { "name": "zendframework/zend-config", @@ -3255,20 +3635,20 @@ "config", "zf2" ], - "time": "2016-02-04 23:01:10" + "time": "2016-02-04T23:01:10+00:00" }, { "name": "zendframework/zend-eventmanager", - "version": "3.1.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-eventmanager.git", - "reference": "c3bce7b7d47c54040b9ae51bc55491c72513b75d" + "reference": "a5e2583a211f73604691586b8406ff7296a946dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/c3bce7b7d47c54040b9ae51bc55491c72513b75d", - "reference": "c3bce7b7d47c54040b9ae51bc55491c72513b75d", + "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/a5e2583a211f73604691586b8406ff7296a946dd", + "reference": "a5e2583a211f73604691586b8406ff7296a946dd", "shasum": "" }, "require": { @@ -3277,7 +3657,7 @@ "require-dev": { "athletic/athletic": "^0.1", "container-interop/container-interop": "^1.1.0", - "phpunit/phpunit": "^5.6", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-stdlib": "^2.7.3 || ^3.0" }, @@ -3288,8 +3668,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev", - "dev-develop": "3.2-dev" + "dev-master": "3.2-dev", + "dev-develop": "3.3-dev" } }, "autoload": { @@ -3309,33 +3689,36 @@ "events", "zf2" ], - "time": "2016-12-19 21:47:12" + "time": "2018-04-25T15:33:34+00:00" }, { "name": "zendframework/zend-filter", - "version": "2.7.2", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-filter.git", - "reference": "b8d0ff872f126631bf63a932e33aa2d22d467175" + "reference": "7b997dbe79459f1652deccc8786d7407fb66caa9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/b8d0ff872f126631bf63a932e33aa2d22d467175", - "reference": "b8d0ff872f126631bf63a932e33aa2d22d467175", + "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/7b997dbe79459f1652deccc8786d7407fb66caa9", + "reference": "7b997dbe79459f1652deccc8786d7407fb66caa9", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "zendframework/zend-stdlib": "^2.7 || ^3.0" + "php": "^5.6 || ^7.0", + "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + }, + "conflict": { + "zendframework/zend-validator": "<2.10.1" }, "require-dev": { - "pear/archive_tar": "^1.4", - "phpunit/phpunit": "^6.0.10 || ^5.7.17", + "pear/archive_tar": "^1.4.3", + "phpunit/phpunit": "^5.7.23 || ^6.4.3", "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-crypt": "^2.6 || ^3.0", - "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", - "zendframework/zend-uri": "^2.5" + "zendframework/zend-crypt": "^3.2.1", + "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", + "zendframework/zend-uri": "^2.6" }, "suggest": { "zendframework/zend-crypt": "Zend\\Crypt component, for encryption filters", @@ -3346,8 +3729,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" + "dev-master": "2.8.x-dev", + "dev-develop": "2.9.x-dev" }, "zf": { "component": "Zend\\Filter", @@ -3364,12 +3747,12 @@ "BSD-3-Clause" ], "description": "provides a set of commonly needed data filters", - "homepage": "https://github.com/zendframework/zend-filter", "keywords": [ + "ZendFramework", "filter", - "zf2" + "zf" ], - "time": "2017-05-17 20:56:17" + "time": "2018-04-11T16:20:04+00:00" }, { "name": "zendframework/zend-hydrator", @@ -3427,28 +3810,28 @@ "hydrator", "zf2" ], - "time": "2016-02-18 22:38:26" + "time": "2016-02-18T22:38:26+00:00" }, { "name": "zendframework/zend-i18n", - "version": "2.7.4", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-i18n.git", - "reference": "d3431e29cc00c2a1c6704e601d4371dbf24f6a31" + "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/d3431e29cc00c2a1c6704e601d4371dbf24f6a31", - "reference": "d3431e29cc00c2a1c6704e601d4371dbf24f6a31", + "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/6d69af5a04e1a4de7250043cb1322f077a0cdb7f", + "reference": "6d69af5a04e1a4de7250043cb1322f077a0cdb7f", "shasum": "" }, "require": { - "php": "^7.0 || ^5.6", + "php": "^5.6 || ^7.0", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "phpunit/phpunit": "^6.0.8 || ^5.7.15", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", "zendframework/zend-cache": "^2.6.1", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-config": "^2.6", @@ -3472,8 +3855,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev", - "dev-develop": "2.8-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" }, "zf": { "component": "Zend\\I18n", @@ -3489,34 +3872,35 @@ "license": [ "BSD-3-Clause" ], - "homepage": "https://github.com/zendframework/zend-i18n", + "description": "Provide translations for your application, and filter and validate internationalized values", "keywords": [ + "ZendFramework", "i18n", - "zf2" + "zf" ], - "time": "2017-05-17 17:00:12" + "time": "2018-05-16T16:39:13+00:00" }, { "name": "zendframework/zend-json", - "version": "3.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-json.git", - "reference": "f42a1588e75c2a3e338cd94c37906231e616daab" + "reference": "4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-json/zipball/f42a1588e75c2a3e338cd94c37906231e616daab", - "reference": "f42a1588e75c2a3e338cd94c37906231e616daab", + "url": "https://api.github.com/repos/zendframework/zend-json/zipball/4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c", + "reference": "4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0" + "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "^2.3", - "zendframework/zend-stdlib": "^2.7 || ^3.0" + "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-stdlib": "^2.7.7 || ^3.1" }, "suggest": { "zendframework/zend-json-server": "For implementing JSON-RPC servers", @@ -3525,8 +3909,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev", - "dev-develop": "3.1-dev" + "dev-master": "3.1.x-dev", + "dev-develop": "3.2.x-dev" } }, "autoload": { @@ -3539,25 +3923,25 @@ "BSD-3-Clause" ], "description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP", - "homepage": "https://github.com/zendframework/zend-json", "keywords": [ + "ZendFramework", "json", - "zf2" + "zf" ], - "time": "2016-04-01 02:34:00" + "time": "2018-01-04T17:51:34+00:00" }, { "name": "zendframework/zend-serializer", - "version": "2.8.0", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-serializer.git", - "reference": "ff74ea020f5f90866eb28365327e9bc765a61a6e" + "reference": "0172690db48d8935edaf625c4cba38b79719892c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-serializer/zipball/ff74ea020f5f90866eb28365327e9bc765a61a6e", - "reference": "ff74ea020f5f90866eb28365327e9bc765a61a6e", + "url": "https://api.github.com/repos/zendframework/zend-serializer/zipball/0172690db48d8935edaf625c4cba38b79719892c", + "reference": "0172690db48d8935edaf625c4cba38b79719892c", "shasum": "" }, "require": { @@ -3566,9 +3950,9 @@ "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "phpunit/phpunit": "^4.5", - "squizlabs/php_codesniffer": "^2.3.1", - "zendframework/zend-math": "^2.6", + "phpunit/phpunit": "^5.7.25 || ^6.4.4", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-math": "^2.6 || ^3.0", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" }, "suggest": { @@ -3578,8 +3962,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev", - "dev-develop": "2.9-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" }, "zf": { "component": "Zend\\Serializer", @@ -3596,25 +3980,25 @@ "BSD-3-Clause" ], "description": "provides an adapter based interface to simply generate storable representation of PHP types by different facilities, and recover", - "homepage": "https://github.com/zendframework/zend-serializer", "keywords": [ + "ZendFramework", "serializer", - "zf2" + "zf" ], - "time": "2016-06-21 17:01:55" + "time": "2018-05-14T18:45:18+00:00" }, { "name": "zendframework/zend-servicemanager", - "version": "2.7.8", + "version": "2.7.11", "source": { "type": "git", "url": "https://github.com/zendframework/zend-servicemanager.git", - "reference": "2ae3b6e4978ec2e9ff52352e661946714ed989f9" + "reference": "99ec9ed5d0f15aed9876433c74c2709eb933d4c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/2ae3b6e4978ec2e9ff52352e661946714ed989f9", - "reference": "2ae3b6e4978ec2e9ff52352e661946714ed989f9", + "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/99ec9ed5d0f15aed9876433c74c2709eb933d4c7", + "reference": "99ec9ed5d0f15aed9876433c74c2709eb933d4c7", "shasum": "" }, "require": { @@ -3653,7 +4037,7 @@ "servicemanager", "zf2" ], - "time": "2016-12-19 19:14:29" + "time": "2018-06-22T14:49:54+00:00" }, { "name": "zendframework/zend-stdlib", @@ -3712,23 +4096,24 @@ "stdlib", "zf2" ], - "time": "2016-04-12 21:17:31" + "time": "2016-04-12T21:17:31+00:00" }, { "name": "zetacomponents/base", - "version": "1.9", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/zetacomponents/Base.git", - "reference": "f20df24e8de3e48b6b69b2503f917e457281e687" + "reference": "489e20235989ddc97fdd793af31ac803972454f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zetacomponents/Base/zipball/f20df24e8de3e48b6b69b2503f917e457281e687", - "reference": "f20df24e8de3e48b6b69b2503f917e457281e687", + "url": "https://api.github.com/repos/zetacomponents/Base/zipball/489e20235989ddc97fdd793af31ac803972454f1", + "reference": "489e20235989ddc97fdd793af31ac803972454f1", "shasum": "" }, "require-dev": { + "phpunit/phpunit": "~5.7", "zetacomponents/unit-test": "*" }, "type": "library", @@ -3775,7 +4160,7 @@ ], "description": "The Base package provides the basic infrastructure that all packages rely on. Therefore every component relies on this package.", "homepage": "https://github.com/zetacomponents", - "time": "2014-09-19 03:28:34" + "time": "2017-11-28T11:30:00+00:00" }, { "name": "zetacomponents/document", @@ -3826,7 +4211,7 @@ ], "description": "The Document components provides a general conversion framework for different semantic document markup languages like XHTML, Docbook, RST and similar.", "homepage": "https://github.com/zetacomponents", - "time": "2013-12-19 11:40:00" + "time": "2013-12-19T11:40:00+00:00" } ], "aliases": [], diff --git a/src/Header/Consumer/AbstractConsumer.php b/src/Header/Consumer/AbstractConsumer.php index c6c99587..c1e624dc 100644 --- a/src/Header/Consumer/AbstractConsumer.php +++ b/src/Header/Consumer/AbstractConsumer.php @@ -41,7 +41,7 @@ abstract class AbstractConsumer * @param ConsumerService $consumerService * @param HeaderPartFactory $partFactory */ - protected function __construct(ConsumerService $consumerService, HeaderPartFactory $partFactory) + public function __construct(ConsumerService $consumerService, HeaderPartFactory $partFactory) { $this->consumerService = $consumerService; $this->partFactory = $partFactory; diff --git a/src/Header/Consumer/AddressConsumer.php b/src/Header/Consumer/AddressConsumer.php index 409fb8f0..4d9ba773 100644 --- a/src/Header/Consumer/AddressConsumer.php +++ b/src/Header/Consumer/AddressConsumer.php @@ -27,7 +27,7 @@ * - To: Winterfell: jonsnow@winterfell.com, Arya Stark ; * * Addresses may contain quoted parts and comments, and names may be mime-header - * encoded (need to review RFC to be sure of this as its been a while). + * encoded. * * @author Zaahid Bateson */ diff --git a/src/Header/Consumer/GenericConsumer.php b/src/Header/Consumer/GenericConsumer.php index 7a136652..66a28d82 100644 --- a/src/Header/Consumer/GenericConsumer.php +++ b/src/Header/Consumer/GenericConsumer.php @@ -160,16 +160,17 @@ private function isSpaceToken(HeaderPart $part) */ protected function filterIgnoredSpaces(array $parts) { + $partsFiltered = array_values(array_filter($parts)); $retParts = []; $spacePart = null; - $count = count($parts); + $count = count($partsFiltered); for ($i = 0; $i < $count; ++$i) { - $part = $parts[$i]; + $part = $partsFiltered[$i]; if ($this->isSpaceToken($part)) { $spacePart = $part; continue; } - $this->addSpaces($parts, $retParts, $i, $spacePart); + $this->addSpaces($partsFiltered, $retParts, $i, $spacePart); $retParts[] = $part; } // ignore trailing spaces diff --git a/src/Header/Consumer/ParameterConsumer.php b/src/Header/Consumer/ParameterConsumer.php index 52517607..4d28117d 100644 --- a/src/Header/Consumer/ParameterConsumer.php +++ b/src/Header/Consumer/ParameterConsumer.php @@ -7,6 +7,8 @@ namespace ZBateson\MailMimeParser\Header\Consumer; use ZBateson\MailMimeParser\Header\Part\Token; +use ZBateson\MailMimeParser\Header\Part\SplitParameterToken; +use ArrayObject; /** * Reads headers separated into parameters consisting of a main value, and @@ -49,23 +51,58 @@ protected function getPartForToken($token, $isLiteral) return $this->partFactory->newToken($token); } + /** + * Adds the passed parameter with the given name and value to a + * SplitParameterToken, at the passed index. If one with the given name + * doesn't exist, it is created. + * + * @param ArrayObject $splitParts + * @param string $name + * @param string $value + * @param int $index + * @param boolean $isEncoded + */ + private function addToSplitPart(ArrayObject $splitParts, $name, $value, $index, $isEncoded) + { + $ret = null; + if (!isset($splitParts[trim($name)])) { + $ret = $this->partFactory->newSplitParameterToken($name); + $splitParts[$name] = $ret; + } + $splitParts[$name]->addPart($value, $isEncoded, $index); + return $ret; + } + /** * Instantiates and returns either a MimeLiteralPart if $strName is empty, - * or a ParameterPart otherwise. + * a SplitParameterToken if the parameter is a split parameter and is the + * first in a series, null if it's a split parameter but is not the first + * part in its series, or a ParameterPart is returned otherwise. + * + * If the part is a SplitParameterToken, it's added to the passed + * $splitParts as well with its name as a key. * * @param string $strName * @param string $strValue - * @return \ZBateson\MailMimeParser\Header\Part\MimeLiteralPart| - * \ZBateson\MailMimeParser\Header\Part\ParameterPart + * @param ArrayObject $splitParts + * @return MimeLiteralPart|SplitParameterToken|ParameterPart */ - private function getPartFor($strName, $strValue) + private function getPartFor($strName, $strValue, ArrayObject $splitParts) { if ($strName === '') { return $this->partFactory->newMimeLiteralPart($strValue); + } elseif (preg_match('~^\s*([^\*]+)\*(\d*)(\*)?$~', $strName, $matches)) { + return $this->addToSplitPart( + $splitParts, + $matches[1], + $strValue, + $matches[2], + (empty($matches[2]) || !empty($matches[3])) + ); } return $this->partFactory->newParameterPart($strName, $strValue); } - + /** * Handles parameter separator tokens during final processing. * @@ -81,10 +118,15 @@ private function getPartFor($strName, $strValue) * @param string $strCat * @return boolean */ - private function processTokenPart($tokenValue, array &$combined, &$strName, &$strCat) - { + private function processTokenPart( + $tokenValue, + ArrayObject $combined, + ArrayObject $splitParts, + &$strName, + &$strCat + ) { if ($tokenValue === ';') { - $combined[] = $this->getPartFor($strName, $strCat); + $combined[] = $this->getPartFor($strName, $strCat, $splitParts); $strName = ''; $strCat = ''; return true; @@ -96,26 +138,51 @@ private function processTokenPart($tokenValue, array &$combined, &$strName, &$st return false; } + /** + * Loops over parts in the passed array, creating ParameterParts out of any + * parsed SplitParameterTokens, replacing them in the array. + * + * The method then calls filterIgnoreSpaces to filter out empty elements in + * the combined array and returns an array. + * + * @param ArrayObject $combined + * @return HeaderPart[]|array + */ + private function finalizeParameterParts(ArrayObject $combined) + { + foreach ($combined as $key => $part) { + if ($part instanceof SplitParameterToken) { + $combined[$key] = $this->partFactory->newParameterPart( + $part->getName(), + $part->getValue(), + $part->getLanguage() + ); + } + } + return $this->filterIgnoredSpaces($combined->getArrayCopy()); + } + /** * Post processing involves creating Part\LiteralPart or Part\ParameterPart * objects out of created Token and LiteralParts. * - * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts - * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[]|array + * @param HeaderPart[] $parts + * @return HeaderPart[]|array */ protected function processParts(array $parts) { - $combined = []; + $combined = new ArrayObject(); + $splitParts = new ArrayObject(); $strCat = ''; $strName = ''; $parts[] = $this->partFactory->newToken(';'); foreach ($parts as $part) { $pValue = $part->getValue(); - if ($part instanceof Token && $this->processTokenPart($pValue, $combined, $strName, $strCat)) { + if ($part instanceof Token && $this->processTokenPart($pValue, $combined, $splitParts, $strName, $strCat)) { continue; } $strCat .= $pValue; } - return $this->filterIgnoredSpaces($combined); + return $this->finalizeParameterParts($combined); } } diff --git a/src/Header/Part/AddressGroupPart.php b/src/Header/Part/AddressGroupPart.php index 9c8167cb..dca280b5 100644 --- a/src/Header/Part/AddressGroupPart.php +++ b/src/Header/Part/AddressGroupPart.php @@ -6,6 +6,8 @@ */ namespace ZBateson\MailMimeParser\Header\Part; +use ZBateson\StreamDecorators\Util\CharsetConverter; + /** * Holds a group of addresses, and an optional group name. * @@ -27,12 +29,13 @@ class AddressGroupPart extends MimeLiteralPart * Creates an AddressGroupPart out of the passed array of AddressParts and an * optional name (which may be mime-encoded). * + * @param CharsetConverter $charsetConverter * @param AddressPart[] $addresses * @param string $name */ - public function __construct(array $addresses, $name = '') + public function __construct(CharsetConverter $charsetConverter, array $addresses, $name = '') { - parent::__construct(trim($name)); + parent::__construct($charsetConverter, trim($name)); $this->addresses = $addresses; } diff --git a/src/Header/Part/AddressPart.php b/src/Header/Part/AddressPart.php index 86ee4410..77fe1170 100644 --- a/src/Header/Part/AddressPart.php +++ b/src/Header/Part/AddressPart.php @@ -6,6 +6,8 @@ */ namespace ZBateson\MailMimeParser\Header\Part; +use ZBateson\StreamDecorators\Util\CharsetConverter; + /** * Holds a single address or name/address pair. * @@ -26,12 +28,14 @@ class AddressPart extends ParameterPart * The passed $name may be mime-encoded. $email is stripped of any * whitespace. * + * @param CharsetConverter $charsetConverter * @param string $name * @param string $email */ - public function __construct($name, $email) + public function __construct(CharsetConverter $charsetConverter, $name, $email) { parent::__construct( + $charsetConverter, $name, '' ); diff --git a/src/Header/Part/CommentPart.php b/src/Header/Part/CommentPart.php index aa968c53..e600c42f 100644 --- a/src/Header/Part/CommentPart.php +++ b/src/Header/Part/CommentPart.php @@ -5,6 +5,7 @@ * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; +use ZBateson\StreamDecorators\Util\CharsetConverter; /** * Represents a mime header comment -- text in a structured mime header @@ -22,11 +23,12 @@ class CommentPart extends MimeLiteralPart /** * Constructs a MimeLiteralPart, decoding the value if it's mime-encoded. * + * @param CharsetConverter $charsetConverter * @param string $token */ - public function __construct($token) + public function __construct(CharsetConverter $charsetConverter, $token) { - parent::__construct($token); + parent::__construct($charsetConverter, $token); $this->comment = $this->value; $this->value = ''; $this->canIgnoreSpacesBefore = true; diff --git a/src/Header/Part/DatePart.php b/src/Header/Part/DatePart.php index c7498a41..0b0e9131 100644 --- a/src/Header/Part/DatePart.php +++ b/src/Header/Part/DatePart.php @@ -6,6 +6,7 @@ */ namespace ZBateson\MailMimeParser\Header\Part; +use ZBateson\StreamDecorators\Util\CharsetConverter; use DateTime; /** @@ -24,13 +25,19 @@ class DatePart extends LiteralPart * Tries parsing the header's value as an RFC 2822 date, and failing that * into an RFC 822 date. * + * @param CharsetConverter $charsetConverter * @param string $token */ - public function __construct($token) { - parent::__construct(trim($token)); - $date = DateTime::createFromFormat(DateTime::RFC2822, $this->value); + public function __construct(CharsetConverter $charsetConverter, $token) { + + // parent::__construct converts character encoding -- may cause problems + // sometimes. + $dateToken = trim($token); + parent::__construct($charsetConverter, $dateToken); + + $date = DateTime::createFromFormat(DateTime::RFC2822, $dateToken); if ($date === false) { - $date = DateTime::createFromFormat(DateTime::RFC822, $this->value); + $date = DateTime::createFromFormat(DateTime::RFC822, $dateToken); } $this->date = ($date === false) ? null : $date; } diff --git a/src/Header/Part/HeaderPart.php b/src/Header/Part/HeaderPart.php index 532a08d6..5e70c557 100644 --- a/src/Header/Part/HeaderPart.php +++ b/src/Header/Part/HeaderPart.php @@ -6,6 +6,8 @@ */ namespace ZBateson\MailMimeParser\Header\Part; +use ZBateson\StreamDecorators\Util\CharsetConverter; + /** * Abstract base class representing a single part of a parsed header. * @@ -17,6 +19,22 @@ abstract class HeaderPart * @var string the value of the part */ protected $value; + + /** + * @var CharsetConverter $charsetConverter the charset converter used for + * converting strings in HeaderPart::convertEncoding + */ + protected $charsetConverter; + + /** + * Sets up dependencies. + * + * @param CharsetConverter $charsetConverter + */ + public function __construct(CharsetConverter $charsetConverter) + { + $this->charsetConverter = $charsetConverter; + } /** * Returns the part's value. @@ -65,13 +83,23 @@ public function ignoreSpacesAfter() /** * Ensures the encoding of the passed string is set to UTF-8. * + * The method does nothing if the passed $from charset is UTF-8 already, or + * if $force is set to false and mb_check_encoding for $str returns true + * for 'UTF-8'. + * * @param string $str + * @param string $from + * @param boolean $force * @return string utf-8 string */ - protected function convertEncoding($str) + protected function convertEncoding($str, $from = 'ISO-8859-1', $force = false) { - if (!mb_check_encoding($str, 'UTF-8')) { - return mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1'); + if ($from !== 'UTF-8') { + // mime header part decoding will force it. This is necessary for + // UTF-7 because mb_check_encoding will return true + if ($force || !mb_check_encoding($str, 'UTF-8')) { + return $this->charsetConverter->convert($str, $from, 'UTF-8'); + } } return $str; } diff --git a/src/Header/Part/HeaderPartFactory.php b/src/Header/Part/HeaderPartFactory.php index 0aa6b55d..5797f6c9 100644 --- a/src/Header/Part/HeaderPartFactory.php +++ b/src/Header/Part/HeaderPartFactory.php @@ -6,6 +6,8 @@ */ namespace ZBateson\MailMimeParser\Header\Part; +use ZBateson\StreamDecorators\Util\CharsetConverter; + /** * Constructs and returns HeaderPart objects. * @@ -13,6 +15,22 @@ */ class HeaderPartFactory { + /** + * @var CharsetConverter $charsetConverter passed to HeaderPart constructors + * for converting strings in HeaderPart::convertEncoding + */ + protected $charsetConverter; + + /** + * Sets up dependencies. + * + * @param CharsetConverter $charsetConverter + */ + public function __construct(CharsetConverter $charsetConverter) + { + $this->charsetConverter = $charsetConverter; + } + /** * Creates and returns a default HeaderPart for this factory, allowing * subclass factories for specialized HeaderParts. @@ -35,7 +53,18 @@ public function newInstance($value) */ public function newToken($value) { - return new Token($value); + return new Token($this->charsetConverter, $value); + } + + /** + * Instantiates and returns a SplitParameterToken with the given name. + * + * @param string $name + * @return SplitParameterToken + */ + public function newSplitParameterToken($name) + { + return new SplitParameterToken($this->charsetConverter, $name); } /** @@ -46,7 +75,7 @@ public function newToken($value) */ public function newLiteralPart($value) { - return new LiteralPart($value); + return new LiteralPart($this->charsetConverter, $value); } /** @@ -57,7 +86,7 @@ public function newLiteralPart($value) */ public function newMimeLiteralPart($value) { - return new MimeLiteralPart($value); + return new MimeLiteralPart($this->charsetConverter, $value); } /** @@ -68,7 +97,7 @@ public function newMimeLiteralPart($value) */ public function newCommentPart($value) { - return new CommentPart($value); + return new CommentPart($this->charsetConverter, $value); } /** @@ -80,7 +109,7 @@ public function newCommentPart($value) */ public function newAddressPart($name, $email) { - return new AddressPart($name, $email); + return new AddressPart($this->charsetConverter, $name, $email); } /** @@ -92,7 +121,7 @@ public function newAddressPart($name, $email) */ public function newAddressGroupPart(array $addresses, $name = '') { - return new AddressGroupPart($addresses, $name); + return new AddressGroupPart($this->charsetConverter, $addresses, $name); } /** @@ -103,7 +132,7 @@ public function newAddressGroupPart(array $addresses, $name = '') */ public function newDatePart($value) { - return new DatePart($value); + return new DatePart($this->charsetConverter, $value); } /** @@ -111,10 +140,11 @@ public function newDatePart($value) * * @param string $name * @param string $value + * @param string $language * @return \ZBateson\MailMimeParser\Header\Part\ParameterPart */ - public function newParameterPart($name, $value) + public function newParameterPart($name, $value, $language = null) { - return new ParameterPart($name, $value); + return new ParameterPart($this->charsetConverter, $name, $value, $language); } } diff --git a/src/Header/Part/LiteralPart.php b/src/Header/Part/LiteralPart.php index f75dae28..2c615b62 100644 --- a/src/Header/Part/LiteralPart.php +++ b/src/Header/Part/LiteralPart.php @@ -7,6 +7,7 @@ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MailMimeParser\Header\Part\HeaderPart; +use ZBateson\StreamDecorators\Util\CharsetConverter; /** * A literal header string part. The value of the part is stripped of CR and LF @@ -19,10 +20,15 @@ class LiteralPart extends HeaderPart /** * Creates a LiteralPart out of the passed string token * + * @param CharsetConverter $charsetConverter * @param string $token */ - public function __construct($token) + public function __construct(CharsetConverter $charsetConverter, $token = null) { - $this->value = preg_replace('/\r|\n/', '', $this->convertEncoding($token)); + parent::__construct($charsetConverter); + $this->value = $token; + if ($token !== null) { + $this->value = preg_replace('/\r|\n/', '', $this->convertEncoding($token)); + } } } diff --git a/src/Header/Part/MimeLiteralPart.php b/src/Header/Part/MimeLiteralPart.php index 90a334bd..7980bf21 100644 --- a/src/Header/Part/MimeLiteralPart.php +++ b/src/Header/Part/MimeLiteralPart.php @@ -6,7 +6,7 @@ */ namespace ZBateson\MailMimeParser\Header\Part; -use ZBateson\MailMimeParser\Stream\Helper\CharsetConverter; +use ZBateson\StreamDecorators\Util\CharsetConverter; /** * Represents a single mime header part token, with the possibility of it being @@ -21,7 +21,7 @@ class MimeLiteralPart extends LiteralPart /** * @var string regex pattern matching a mime-encoded part */ - const MIME_PART_PATTERN = '=\?[A-Za-z\-_0-9]+\?[QBqb]\?[^\?]+\?='; + const MIME_PART_PATTERN = '=\?[A-Za-z\-_0-9\*]+\?[QBqb]\?[^\?]+\?='; /** * @var bool set to true to ignore spaces before this part @@ -33,15 +33,26 @@ class MimeLiteralPart extends LiteralPart */ protected $canIgnoreSpacesAfter = false; + /** + * @var array maintains an array mapping rfc1766 language tags to parts of + * text in the value. + * + * Each array element is an array containing two elements, one with key + * 'lang', and another with key 'value'. + */ + protected $languages = []; + /** * Decoding the passed token value if it's mime-encoded and assigns the * decoded value to a member variable. Sets canIgnoreSpacesBefore and * canIgnoreSpacesAfter. * + * @param CharsetConverter $charsetConverter * @param string $token */ - public function __construct($token) + public function __construct(CharsetConverter $charsetConverter, $token) { + parent::__construct($charsetConverter); $this->value = $this->decodeMime($token); // preg_match returns int $pattern = self::MIME_PART_PATTERN; @@ -62,15 +73,38 @@ public function __construct($token) protected function decodeMime($value) { $pattern = self::MIME_PART_PATTERN; + // remove whitespace between two adjacent mime encoded parts $value = preg_replace("/($pattern)\\s+(?=$pattern)/", '$1', $value); - $aMimeParts = preg_split("/($pattern)/", $value, -1, PREG_SPLIT_DELIM_CAPTURE); + // with PREG_SPLIT_DELIM_CAPTURE, matched and unmatched parts are returned + $aMimeParts = preg_split("/($pattern)/", $value, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); $ret = ''; foreach ($aMimeParts as $entity) { - $ret .= $this->decodeMatchedEntity($entity); + $ret .= $this->decodeSplitPart($entity); } return $ret; } + /** + * Decodes a matched mime entity part into a string and returns it, after + * adding the string into the languages array. + * + * @param string[] $matches + * @return string + */ + private function decodeMatchedEntity($matches) + { + $body = $matches[4]; + if (strtoupper($matches[3]) === 'Q') { + $body = quoted_printable_decode(str_replace('_', '=20', $body)); + } else { + $body = base64_decode($body); + } + $language = $matches[2]; + $decoded = $this->convertEncoding($body, $matches[1], true); + $this->addToLanguage($decoded, $language); + return $decoded; + } + /** * Decodes a single mime-encoded entity. * @@ -84,19 +118,14 @@ protected function decodeMime($value) * @param string $entity * @return string */ - private function decodeMatchedEntity($entity) + private function decodeSplitPart($entity) { - if (preg_match("/^=\?([A-Za-z\-_0-9]+)\?([QBqb])\?([^\?]+)\?=$/", $entity, $matches)) { - $body = $matches[3]; - if (strtoupper($matches[2]) === 'Q') { - $body = quoted_printable_decode(str_replace('_', '=20', $body)); - } else { - $body = base64_decode($body); - } - $converter = new CharsetConverter($matches[1], 'UTF-8'); - return $converter->convert($body); + if (preg_match("/^=\?([A-Za-z\-_0-9]+)\*?([A-Za-z\-_0-9]+)?\?([QBqb])\?([^\?]+)\?=$/", $entity, $matches)) { + return $this->decodeMatchedEntity($matches); } - return $this->convertEncoding($entity); + $decoded = $this->convertEncoding($entity); + $this->addToLanguage($decoded); + return $decoded; } /** @@ -124,4 +153,42 @@ public function ignoreSpacesAfter() { return $this->canIgnoreSpacesAfter; } + + /** + * Adds the passed part into the languages array with the given language. + * + * @param string $part + * @param string|null $language + */ + protected function addToLanguage($part, $language = null) + { + $this->languages[] = [ + 'lang' => $language, + 'value' => $part + ]; + } + + /** + * Returns an array of parts mapped to languages in the header value, for + * instance the string: + * + * 'Hello and =?UTF-8*fr-be?Q?bonjour_?= =?UTF-8*it?Q?mi amici?=. Welcome!' + * + * Would be mapped in the returned array as follows: + * + * ```php + * [ + * 0 => [ 'lang' => null, 'value' => 'Hello and ' ], + * 1 => [ 'lang' => 'fr-be', 'value' => 'bonjour ' ], + * 3 => [ 'lang' => 'it', 'value' => 'mi amici' ], + * 4 => [ 'lang' => null, 'value' => ' Weolcome!' ] + * ] + * ``` + * + * @return string[][] + */ + public function getLanguageArray() + { + return $this->languages; + } } diff --git a/src/Header/Part/ParameterPart.php b/src/Header/Part/ParameterPart.php index d6f2a4d0..0ae0e213 100644 --- a/src/Header/Part/ParameterPart.php +++ b/src/Header/Part/ParameterPart.php @@ -6,6 +6,8 @@ */ namespace ZBateson\MailMimeParser\Header\Part; +use ZBateson\StreamDecorators\Util\CharsetConverter; + /** * Represents a name/value pair part of a header. * @@ -18,17 +20,34 @@ class ParameterPart extends MimeLiteralPart */ protected $name; + /** + * @var string the RFC-1766 language tag if set. + */ + protected $language; + /** * Constructs a ParameterPart out of a name/value pair. The name and * value are both mime-decoded if necessary. * + * If $language is provided, $name and $value are not mime-decoded. Instead, + * they're taken as literals as part of a SplitParameterToken. + * + * @param CharsetConverter $charsetConverter * @param string $name * @param string $value + * @param string $language */ - public function __construct($name, $value) + public function __construct(CharsetConverter $charsetConverter, $name, $value, $language = null) { - parent::__construct(trim($value)); - $this->name = $this->decodeMime(trim($name)); + if ($language !== null) { + parent::__construct($charsetConverter, ''); + $this->name = $name; + $this->value = $value; + $this->language = $language; + } else { + parent::__construct($charsetConverter, trim($value)); + $this->name = $this->decodeMime(trim($name)); + } } /** @@ -40,4 +59,15 @@ public function getName() { return $this->name; } + + /** + * Returns the RFC-1766 (or subset) language tag, if the parameter is a + * split RFC-2231 part with a language tag set. + * + * @return string + */ + public function getLanguage() + { + return $this->language; + } } diff --git a/src/Header/Part/SplitParameterToken.php b/src/Header/Part/SplitParameterToken.php new file mode 100644 index 00000000..6420d09c --- /dev/null +++ b/src/Header/Part/SplitParameterToken.php @@ -0,0 +1,187 @@ +name = trim($name); + } + + /** + * Extracts charset and language from an encoded value, setting them on the + * current object if $index is 0 and adds the value part to the encodedParts + * array. + * + * @param string $value + * @param int $index + */ + protected function extractMetaInformationAndValue($value, $index) + { + if (preg_match('~^([^\']*)\'([^\']*)\'(.*)$~', $value, $matches)) { + if ($index === 0) { + $this->charset = (!empty($matches[1])) ? $matches[1] : $this->charset; + $this->language = (!empty($matches[2])) ? $matches[2] : $this->language; + } + $value = $matches[3]; + } + $this->encodedParts[$index] = $value; + } + + /** + * Adds the passed part to the running array of values. + * + * If $isEncoded is true, language and charset info is extracted from the + * value, and the value is decoded before returning in getValue. + * + * The value of the parameter is sorted based on the passed $index + * arguments when adding before concatenating when re-constructing the + * value. + * + * @param string $value + * @param boolean $isEncoded + * @param int $index + */ + public function addPart($value, $isEncoded, $index) + { + if (empty($index)) { + $index = 0; + } + if ($isEncoded) { + $this->extractMetaInformationAndValue($value, $index); + } else { + $this->literalParts[$index] = $this->convertEncoding($value); + } + } + + /** + * Traverses $this->encodedParts until a non-sequential key is found, or the + * end of the array is found. + * + * This allows encoded parts of a split parameter to be split anywhere and + * reconstructed. + * + * The returned string is converted to UTF-8 before being returned. + * + * @return string + */ + private function getNextEncodedValue() + { + $cur = current($this->encodedParts); + $key = key($this->encodedParts); + $running = ''; + while ($cur !== false) { + $running .= $cur; + $cur = next($this->encodedParts); + $nKey = key($this->encodedParts); + if ($nKey !== $key + 1) { + break; + } + $key = $nKey; + } + return $this->convertEncoding( + rawurldecode($running), + $this->charset, + true + ); + } + + /** + * Reconstructs the value of the split parameter into a single UTF-8 string + * and returns it. + * + * @return string + */ + public function getValue() + { + $parts = $this->literalParts; + + reset($this->encodedParts); + ksort($this->encodedParts); + while (current($this->encodedParts) !== false) { + $parts[key($this->encodedParts)] = $this->getNextEncodedValue(); + } + + ksort($parts); + return array_reduce( + $parts, + function ($carry, $item) { + return $carry . $item; + }, + '' + ); + } + + /** + * Returns the name of the parameter. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the language of the parameter if set, or null if not. + * + * @return string + */ + public function getLanguage() + { + return $this->language; + } +} diff --git a/src/Header/Part/Token.php b/src/Header/Part/Token.php index d5e6cf12..7e35aa6c 100644 --- a/src/Header/Part/Token.php +++ b/src/Header/Part/Token.php @@ -7,6 +7,7 @@ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MailMimeParser\Header\Part\HeaderPart; +use ZBateson\StreamDecorators\Util\CharsetConverter; /** * Holds a string value token that will require additional processing by a @@ -24,10 +25,12 @@ class Token extends HeaderPart /** * Initializes a token. * + * @param CharsetConverter $charsetConverter * @param string $value the token's value */ - public function __construct($value) + public function __construct(CharsetConverter $charsetConverter, $value) { + parent::__construct($charsetConverter); $this->value = $value; } diff --git a/src/MailMimeParser.php b/src/MailMimeParser.php index f6903e74..afadaead 100644 --- a/src/MailMimeParser.php +++ b/src/MailMimeParser.php @@ -6,6 +6,8 @@ */ namespace ZBateson\MailMimeParser; +use GuzzleHttp\Psr7; + /** * Parses a MIME message into a \ZBateson\MailMimeParser\Message object. * @@ -20,6 +22,11 @@ */ class MailMimeParser { + /** + * @var string defines the default charset used by MessagePart. + */ + const DEFAULT_CHARSET = 'UTF-8'; + /** * @var \ZBateson\MailMimeParser\SimpleDi dependency injection container */ @@ -33,6 +40,28 @@ public function __construct() $this->di = SimpleDi::singleton(); } + /** + * Sets the default charset used by MMP for strings returned by read + * operations on text content (e.g. MessagePart::getContentResourceHandle, + * getContent, etc...) + * + * @param string $charset + */ + public static function setDefaultCharset($charset) + { + self::$defaultCharset = $charset; + } + + /** + * Returns the default charset that will be used by MMP strings returned. + * + * @return string + */ + public static function getDefaultCharset() + { + return self::$defaultCharset; + } + /** * Parses the passed stream handle into a ZBateson\MailMimeParser\Message * object and returns it. @@ -48,16 +77,15 @@ public function __construct() */ public function parse($handleOrString) { - // $tempHandle is attached to $message, and closed in its destructor - $tempHandle = fopen('php://temp', 'r+'); - if (is_string($handleOrString)) { - fwrite($tempHandle, $handleOrString); - } else { - stream_copy_to_stream($handleOrString, $tempHandle); - } - rewind($tempHandle); + $stream = Psr7\stream_for($handleOrString); + $copy = Psr7\stream_for(fopen('php://temp', 'r+')); + + Psr7\copy_to_stream($stream, $copy); + $copy->rewind(); + + // don't close it when $stream gets destroyed + $stream->detach(); $parser = $this->di->newMessageParser(); - $message = $parser->parse($tempHandle); - return $message; + return $parser->parse($copy); } } diff --git a/src/Message.php b/src/Message.php index 4cf67326..fec7be4f 100644 --- a/src/Message.php +++ b/src/Message.php @@ -6,11 +6,15 @@ */ namespace ZBateson\MailMimeParser; +use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Header\HeaderFactory; -use ZBateson\MailMimeParser\Message\MimePart; -use ZBateson\MailMimeParser\Message\MimePartFactory; -use ZBateson\MailMimeParser\Message\Writer\MessageWriter; +use ZBateson\MailMimeParser\Message\Helper\MessageHelperService; +use ZBateson\MailMimeParser\Message\Part\MimePart; +use ZBateson\MailMimeParser\Message\Part\PartBuilder; +use ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager; use ZBateson\MailMimeParser\Message\PartFilter; +use ZBateson\MailMimeParser\Message\PartFilterFactory; +use ZBateson\MailMimeParser\Stream\StreamFactory; /** * A parsed mime message with optional mime parts depending on its type. @@ -23,29 +27,43 @@ class Message extends MimePart { /** - * @var string unique ID used to identify the object to - * $this->partStreamRegistry when registering the stream. The ID is - * used for opening stream parts with the mmp-mime-message "protocol". - * - * @see \ZBateson\MailMimeParser\SimpleDi::registerStreamExtensions - * @see \ZBateson\MailMimeParser\Stream\PartStream::stream_open + * @var MessageHelperService helper class with various message manipulation + * routines. */ - protected $objectId; + protected $messageHelperService; /** - * @var \ZBateson\MailMimeParser\Message\MimePartFactory a MimePartFactory to create - * parts for attachments/content - */ - protected $mimePartFactory; - - /** - * @var \ZBateson\MailMimeParser\Message\Writer\MessageWriter the part - * writer for this Message. The same object is assigned to $partWriter - * but as an AbstractWriter -- not really needed in PHP but helps with - * auto-complete and code analyzers. + * @param PartStreamFilterManager $partStreamFilterManager + * @param StreamFactory $streamFactory + * @param PartFilterFactory $partFilterFactory + * @param HeaderFactory $headerFactory + * @param PartBuilder $partBuilder + * @param MessageHelperService $messageHelperService + * @param StreamInterface $stream + * @param StreamInterface $contentStream */ - protected $messageWriter = null; - + public function __construct( + PartStreamFilterManager $partStreamFilterManager, + StreamFactory $streamFactory, + PartFilterFactory $partFilterFactory, + HeaderFactory $headerFactory, + PartBuilder $partBuilder, + MessageHelperService $messageHelperService, + StreamInterface $stream = null, + StreamInterface $contentStream = null + ) { + parent::__construct( + $partStreamFilterManager, + $streamFactory, + $partFilterFactory, + $headerFactory, + $partBuilder, + $stream, + $contentStream + ); + $this->messageHelperService = $messageHelperService; + } + /** * Convenience method to parse a handle or string into a Message without * requiring including MailMimeParser, instantiating it, and calling parse. @@ -58,47 +76,18 @@ public static function from($handleOrString) $mmp = new MailMimeParser(); return $mmp->parse($handleOrString); } - - /** - * Constructs a Message. - * - * @param HeaderFactory $headerFactory - * @param MessageWriter $messageWriter - * @param MimePartFactory $mimePartFactory - */ - public function __construct( - HeaderFactory $headerFactory, - MessageWriter $messageWriter, - MimePartFactory $mimePartFactory - ) { - parent::__construct($headerFactory, $messageWriter); - $this->messageWriter = $messageWriter; - $this->mimePartFactory = $mimePartFactory; - $this->objectId = uniqid(); - } - - /** - * Returns the unique object ID registered with the PartStreamRegistry - * service object. - * - * @return string - */ - public function getObjectId() - { - return $this->objectId; - } /** * Returns the text/plain part at the given index (or null if not found.) * * @param int $index - * @return \ZBateson\MailMimeParser\Message\MimePart + * @return \ZBateson\MailMimeParser\Message\Part\MimePart */ public function getTextPart($index = 0) { return $this->getPart( $index, - PartFilter::fromInlineContentType('text/plain') + $this->partFilterFactory->newFilterFromInlineContentType('text/plain') ); } @@ -109,20 +98,22 @@ public function getTextPart($index = 0) */ public function getTextPartCount() { - return $this->getPartCount(PartFilter::fromInlineContentType('text/plain')); + return $this->getPartCount( + $this->partFilterFactory->newFilterFromInlineContentType('text/plain') + ); } /** * Returns the text/html part at the given index (or null if not found.) * * @param $index - * @return \ZBateson\MailMimeParser\Message\MimePart + * @return \ZBateson\MailMimeParser\Message\Part\MimePart */ public function getHtmlPart($index = 0) { return $this->getPart( $index, - PartFilter::fromInlineContentType('text/html') + $this->partFilterFactory->newFilterFromInlineContentType('text/html') ); } @@ -133,788 +124,257 @@ public function getHtmlPart($index = 0) */ public function getHtmlPartCount() { - return $this->getPartCount(PartFilter::fromInlineContentType('text/html')); - } - - /** - * Returns the content MimePart, which could be a text/plain part, - * text/html part, multipart/alternative part, or null if none is set. - * - * This function is deprecated in favour of getTextPart/getHtmlPart and - * getPartByMimeType. - * - * @deprecated since version 0.4.2 - * @return \ZBateson\MailMimeParser\Message\MimePart - */ - public function getContentPart() - { - $alternative = $this->getPartByMimeType('multipart/alternative'); - if ($alternative !== null) { - return $alternative; - } - $text = $this->getTextPart(); - return ($text !== null) ? $text : $this->getHtmlPart(); - } - - /** - * Returns an open resource handle for the passed string or resource handle. - * - * For a string, creates a php://temp stream and returns it. - * - * @param resource|string $stringOrHandle - * @return resource - */ - private function getHandleForStringOrHandle($stringOrHandle) - { - $tempHandle = fopen('php://temp', 'r+'); - if (is_string($stringOrHandle)) { - fwrite($tempHandle, $stringOrHandle); - } else { - stream_copy_to_stream($stringOrHandle, $tempHandle); - } - rewind($tempHandle); - return $tempHandle; - } - - /** - * Creates and returns a unique boundary. - * - * @param string $mimeType first 3 characters of a multipart type are used, - * e.g. REL for relative or ALT for alternative - * @return string - */ - private function getUniqueBoundary($mimeType) - { - $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-'); - return uniqid('----=MMP-' . $type . $this->objectId . '.', true); - } - - /** - * Creates a unique mime boundary and assigns it to the passed part's - * Content-Type header with the passed mime type. - * - * @param \ZBateson\MailMimeParser\Message\MimePart $part - * @param string $mimeType - */ - private function setMimeHeaderBoundaryOnPart(MimePart $part, $mimeType) - { - $part->setRawHeader( - 'Content-Type', - "$mimeType;\r\n\tboundary=\"" - . $this->getUniqueBoundary($mimeType) . '"' + return $this->getPartCount( + $this->partFilterFactory->newFilterFromInlineContentType('text/html') ); } - - /** - * Sets this message to be a multipart/alternative message, making space for - * a second content part. - * - * Creates a content part and assigns the content stream from the message to - * that newly created part. - */ - private function setMessageAsAlternative() - { - $contentPart = $this->mimePartFactory->newMimePart(); - $contentPart->attachContentResourceHandle($this->handle); - $this->detachContentResourceHandle(); - $contentType = 'text/plain; charset="us-ascii"'; - $contentHeader = $this->getHeader('Content-Type'); - if ($contentHeader !== null) { - $contentType = $contentHeader->getRawValue(); - } - $contentPart->setRawHeader('Content-Type', $contentType); - $this->setMimeHeaderBoundaryOnPart($this, 'multipart/alternative'); - $this->addPart($contentPart, 0); - } /** - * Returns the direct child of $alternativePart containing a part of - * $mimeType. - * - * Used for alternative mime types that have a multipart/mixed or - * multipart/related child containing a content part of $mimeType, where - * the whole mixed/related part should be removed. - * - * @param string $mimeType the content-type to find below $alternativePart - * @param MimePart $alternativePart The multipart/alternative part to look - * under - * @return boolean|MimePart false if a part is not found - */ - private function getContentPartContainerFromAlternative($mimeType, MimePart $alternativePart) - { - $part = $alternativePart->getPart(0, PartFilter::fromInlineContentType($mimeType)); - $contPart = null; - do { - if ($part === null) { - return false; - } - $contPart = $part; - $part = $part->getParent(); - } while ($part !== $alternativePart); - return $contPart; - } - - /** - * Moves all parts under $from into this message except those with a - * content-type equal to $exceptMimeType. If the message is not a - * multipart/mixed message, it is set to multipart/mixed first. + * Returns the attachment part at the given 0-based index, or null if none + * is set. * - * @param MimePart $from - * @param string $exceptMimeType + * @param int $index + * @return MessagePart */ - private function moveAllPartsAsAttachmentsExcept(MimePart $from, $exceptMimeType) + public function getAttachmentPart($index) { - $parts = $from->getAllParts(new PartFilter([ - 'multipart' => PartFilter::FILTER_EXCLUDE, - 'headers' => [ - PartFilter::FILTER_EXCLUDE => [ - 'Content-Type' => $exceptMimeType - ] - ] - ])); - if ($this->getHeaderValue('Content-Type') !== 'multipart/mixed') { - $this->setMessageAsMixed(); - } - foreach ($parts as $part) { - $from->removePart($part); - $this->addPart($part); + $attachments = $this->getAllAttachmentParts(); + if (!isset($attachments[$index])) { + return null; } + return $attachments[$index]; } /** - * Removes all parts of $mimeType from $alternativePart. + * Returns all attachment parts. * - * If $alternativePart contains a multipart/mixed or multipart/relative part - * with other parts of different content-types, the multipart part is - * removed, and parts of different content-types can optionally be moved to - * the main message part. + * "Attachments" are any non-multipart, non-signature and any text or html + * html part witha Content-Disposition set to 'attachment'. * - * @param string $mimeType - * @param MimePart $alternativePart - * @param bool $keepOtherContent - * @return bool + * @return MessagePart[] */ - private function removeAllContentPartsFromAlternative($mimeType, $alternativePart, $keepOtherContent) + public function getAllAttachmentParts() { - $rmPart = $this->getContentPartContainerFromAlternative($mimeType, $alternativePart); - if ($rmPart === false) { - return false; - } - if ($keepOtherContent) { - $this->moveAllPartsAsAttachmentsExcept($rmPart, $mimeType); - $alternativePart = $this->getPart(0, PartFilter::fromInlineContentType('multipart/alternative')); - } else { - $rmPart->removeAllParts(); - } - $this->removePart($rmPart); - if ($alternativePart !== null) { - if ($alternativePart->getChildCount() === 1) { - $this->replacePart($alternativePart, $alternativePart->getChild(0)); - } elseif ($alternativePart->getChildCount() === 0) { - $this->removePart($alternativePart); + $parts = $this->getAllParts( + $this->partFilterFactory->newFilterFromArray([ + 'multipart' => PartFilter::FILTER_EXCLUDE + ]) + ); + return array_values(array_filter( + $parts, + function ($part) { + return !( + $part->isTextPart() + && $part->getContentDisposition() === 'inline' + ); } - } - while ($this->getChildCount() === 1) { - $this->replacePart($this, $this->getChild(0)); - } - return true; - } - - /** - * Removes the content part of the message with the passed mime type. If - * there is a remaining content part and it is an alternative part of the - * main message, the content part is moved to the message part. - * - * If the content part is part of an alternative part beneath the message, - * the alternative part is replaced by the remaining content part, - * optionally keeping other parts if $keepOtherContent is set to true. - * - * @param string $mimeType - * @param bool $keepOtherContent - * @return boolean true on success - */ - protected function removeAllContentPartsByMimeType($mimeType, $keepOtherContent = false) - { - $alt = $this->getPart(0, PartFilter::fromInlineContentType('multipart/alternative')); - if ($alt !== null) { - return $this->removeAllContentPartsFromAlternative($mimeType, $alt, $keepOtherContent); - } - $this->removeAllParts(PartFilter::fromInlineContentType($mimeType)); - return true; - } - - /** - * Removes the 'inline' part with the passed contentType, at the given index - * defaulting to the first - * - * @param string $contentType - * @param int $index - * @return boolean true on success - */ - protected function removePartByMimeType($mimeType, $index = 0) - { - $parts = $this->getAllParts(PartFilter::fromInlineContentType($mimeType)); - $alt = $this->getPart(0, PartFilter::fromInlineContentType('multipart/alternative')); - if ($parts === null || !isset($parts[$index])) { - return false; - } elseif (count($parts) === 1) { - return $this->removeAllContentPartsByMimeType($mimeType, true); - } - $part = $parts[$index]; - $this->removePart($part); - if ($alt !== null && $alt->getChildCount() === 1) { - $this->replacePart($alt, $alt->getChild(0)); - } - return true; - } - - /** - * Creates a new mime part as a multipart/alternative and assigns the passed - * $contentPart as a part below it before returning it. - * - * @param MimePart $contentPart - * @return MimePart the alternative part - */ - private function createAlternativeContentPart(MimePart $contentPart) - { - $altPart = $this->mimePartFactory->newMimePart(); - $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative'); - $this->removePart($contentPart); - $this->addPart($altPart, 0); - $altPart->addPart($contentPart, 0); - return $altPart; + )); } /** - * Copies type headers (Content-Type, Content-Disposition, - * Content-Transfer-Encoding) from the $from MimePart to $to. Attaches the - * content resource handle of $from to $to, and loops over child parts, - * removing them from $from and adding them to $to. + * Returns the number of attachments available. * - * @param MimePart $from - * @param MimePart $to + * @return int */ - private function movePartContentAndChildrenToPart(MimePart $from, MimePart $to) + public function getAttachmentCount() { - $this->copyTypeHeadersFromPartToPart($from, $to); - $to->attachContentResourceHandle($from->getContentResourceHandle()); - $from->detachContentResourceHandle(); - foreach ($from->getChildParts() as $child) { - $from->removePart($child); - $to->addPart($child); - } + return count($this->getAllAttachmentParts()); } /** - * Replaces the $part MimePart with $replacement. - * - * Essentially removes $part from its parent, and adds $replacement in its - * same position. If $part is this Message, its type headers are moved from - * this message to $replacement, the content resource is moved, and children - * are assigned to $replacement. + * Returns a resource handle where the 'inline' text/plain content at the + * passed $index can be read or null if unavailable. * - * @param MimePart $part - * @param MimePart $replacement + * @param int $index + * @param string $charset + * @return resource */ - private function replacePart(MimePart $part, MimePart $replacement) + public function getTextStream($index = 0, $charset = MailMimeParser::DEFAULT_CHARSET) { - $this->removePart($replacement); - if ($part === $this) { - $this->movePartContentAndChildrenToPart($replacement, $part); - return; + $textPart = $this->getTextPart($index); + if ($textPart !== null) { + return $textPart->getContentResourceHandle($charset); } - $parent = $part->getParent(); - $position = $parent->removePart($part); - $parent->addPart($replacement, $position); + return null; } /** - * Copies Content-Type, Content-Disposition and Content-Transfer-Encoding - * headers from the $from header into the $to header. If the Content-Type - * header isn't defined in $from, defaults to text/plain and - * quoted-printable. - * - * @param \ZBateson\MailMimeParser\Message\MimePart $from - * @param \ZBateson\MailMimeParser\Message\MimePart $to - */ - private function copyTypeHeadersFromPartToPart(MimePart $from, MimePart $to) - { - $typeHeader = $from->getHeader('Content-Type'); - if ($typeHeader !== null) { - $to->setRawHeader('Content-Type', $typeHeader->getRawValue()); - $encodingHeader = $from->getHeader('Content-Transfer-Encoding'); - if ($encodingHeader !== null) { - $to->setRawHeader('Content-Transfer-Encoding', $encodingHeader->getRawValue()); - } - $dispositionHeader = $from->getHeader('Content-Disposition'); - if ($dispositionHeader !== null) { - $to->setRawHeader('Content-Disposition', $dispositionHeader->getRawValue()); - } - } else { - $to->setRawHeader('Content-Type', 'text/plain;charset=us-ascii'); - $to->setRawHeader('Content-Transfer-Encoding', 'quoted-printable'); - } - } - - /** - * Creates a new content part from the passed part, allowing the part to be - * used for something else (e.g. changing a non-mime message to a multipart - * mime message). - * - * @param MimePart $part - * @return MimePart the newly-created MimePart - */ - private function createNewContentPartFromPart(MimePart $part) - { - $contPart = $this->mimePartFactory->newMimePart(); - $this->copyTypeHeadersFromPartToPart($part, $contPart); - $contPart->attachContentResourceHandle($part->handle); - $part->detachContentResourceHandle(); - return $contPart; - } - - /** - * Creates a new part out of the current contentPart and sets the message's - * type to be multipart/mixed. - */ - private function setMessageAsMixed() - { - if ($this->isMultiPart()) { - $part = $this->mimePartFactory->newMimePart(); - $this->movePartContentAndChildrenToPart($this, $part); - $this->addPart($part, 0); - } elseif ($this->handle !== null) { - $part = $this->createNewContentPartFromPart($this); - $this->addPart($part, 0); - } - $this->setMimeHeaderBoundaryOnPart($this, 'multipart/mixed'); - $this->removeHeader('Content-Transfer-Encoding'); - $this->removeHeader('Content-Disposition'); - } - - /** - * This function makes space by moving the main message part down one level. + * Returns the content of the inline text/plain part at the given index. * - * The content-type, content-disposition and content-transfer-encoding - * headers are copied from this message to the newly created part, the - * resource handle is moved and detached, any attachments and content parts - * with parents set to this message get their parents set to the newly - * created part. - */ - private function makeSpaceForMultipartSignedMessage() - { - $this->enforceMime(); - $messagePart = $this->mimePartFactory->newMimePart(); - - $this->copyTypeHeadersFromPartToPart($this, $messagePart); - $messagePart->attachContentResourceHandle($this->handle); - $this->detachContentResourceHandle(); - - foreach ($this->getChildParts() as $part) { - $this->removePart($part); - $messagePart->addPart($part); - } - $this->addPart($messagePart, 0); - } - - /** - * Creates and returns a new MimePart for the signature part of a - * multipart/signed message + * Reads the entire stream content into a string and returns it. Returns + * null if the message doesn't have an inline text part. * - * @param string $body + * @param int $index + * @param string $charset + * @return string */ - public function createSignaturePart($body) + public function getTextContent($index = 0, $charset = MailMimeParser::DEFAULT_CHARSET) { - $signedPart = $this->getSignaturePart(); - if ($signedPart === null) { - $signedPart = $this->mimePartFactory->newMimePart(); - $this->addPart($signedPart); + $part = $this->getTextPart($index); + if ($part !== null) { + return $part->getContent($charset); } - $signedPart->setRawHeader( - 'Content-Type', - $this->getHeaderParameter('Content-Type', 'protocol') - ); - $signedPart->setContent($body); + return null; } /** - * Loops over parts of this message and sets the content-transfer-encoding - * header to quoted-printable for text/* mime parts, and to base64 - * otherwise for parts that are '8bit' encoded. - * - * Used for multipart/signed messages which doesn't support 8bit transfer - * encodings. - */ - private function overwrite8bitContentEncoding() - { - $parts = $this->getAllParts(new PartFilter([ - 'headers' => [ PartFilter::FILTER_INCLUDE => [ - 'Content-Transfer-Encoding' => '8bit' - ] ] - ])); - foreach ($parts as $part) { - $contentType = strtolower($part->getHeaderValue('Content-Type', 'text/plain')); - if ($contentType === 'text/plain' || $contentType === 'text/html') { - $part->setRawHeader('Content-Transfer-Encoding', 'quoted-printable'); - } else { - $part->setRawHeader('Content-Transfer-Encoding', 'base64'); - } - } - } - - /** - * Ensures a non-text part comes first in a signed multipart/alternative - * message as some clients seem to prefer the first content part if the - * client doesn't understand multipart/signed. - */ - private function ensureHtmlPartFirstForSignedMessage() - { - $alt = $this->getPartByMimeType('multipart/alternative'); - if ($alt !== null) { - $cont = $this->getContentPartContainerFromAlternative('text/html', $alt); - $pos = array_search($cont, $alt->parts, true); - if ($pos !== false && $pos !== 0) { - $tmp = $alt->parts[0]; - $alt->parts[0] = $alt->parts[$pos]; - $alt->parts[$pos] = $tmp; - } - } - } - - /** - * Turns the message into a multipart/signed message, moving the actual - * message into a child part, sets the content-type of the main message to - * multipart/signed and adds a signature part as well. - * - * @param string $micalg The Message Integrity Check algorithm being used - * @param string $protocol The mime-type of the signature body - */ - public function setAsMultipartSigned($micalg, $protocol) - { - $contentType = $this->getHeaderValue('Content-Type', 'text/plain'); - if (strcasecmp($contentType, 'multipart/signed') !== 0) { - $this->makeSpaceForMultipartSignedMessage(); - $this->removeHeader('Content-Disposition'); - $this->removeHeader('Content-Transfer-Encoding'); - } - $boundary = $this->getUniqueBoundary('multipart/signed'); - $this->setRawHeader( - 'Content-Type', - "multipart/signed;\r\n\tboundary=\"$boundary\";\r\n\tmicalg=\"$micalg\"; protocol=\"$protocol\"" - ); - $this->overwrite8bitContentEncoding(); - $this->ensureHtmlPartFirstForSignedMessage(); - $this->createSignaturePart('Not set'); - } - - /** - * Returns the signed part or null if not set. - * - * @return \ZBateson\MailMimeParser\Message\MimePart - */ - public function getSignaturePart() - { - $contentType = $this->getHeaderValue('Content-Type', 'text/plain'); - if (strcasecmp($contentType, 'multipart/signed') === 0) { - return $this->getChild(1); - } else { - return null; - } - } - - /** - * Returns a string containing the original message's signed part, useful - * for verifying the email. - * - * If the signed part of the message ends in a final empty line, the line is - * removed as it's considered part of the signature's mime boundary. From - * RFC-3156: - * - * Note: The accepted OpenPGP convention is for signed data to end - * with a sequence. Note that the sequence - * immediately preceding a MIME boundary delimiter line is considered - * to be part of the delimiter in [3], 5.1. Thus, it is not part of - * the signed data preceding the delimiter line. An implementation - * which elects to adhere to the OpenPGP convention has to make sure - * it inserts a pair on the last line of the data to be - * signed and transmitted (signed message and transmitted message - * MUST be identical). - * - * The additional line should be inserted by the signer -- for verification - * purposes if it's missing, it would seem the content part would've been - * signed without a last . + * Returns a resource handle where the 'inline' text/html content at the + * passed $index can be read or null if unavailable. * - * @return string or null if the message doesn't have any children, or the - * child returns null for getOriginalStreamHandle + * @param int $index + * @param string $charset + * @return resource */ - public function getOriginalMessageStringForSignatureVerification() + public function getHtmlStream($index = 0, $charset = MailMimeParser::DEFAULT_CHARSET) { - $child = $this->getChild(0); - if ($child !== null && $child->getOriginalStreamHandle() !== null) { - $normalized = preg_replace( - '/\r\n|\r|\n/', - "\r\n", - stream_get_contents($child->getOriginalStreamHandle()) - ); - $len = strlen($normalized); - if ($len > 0 && strrpos($normalized, "\r\n") == $len - 2) { - return substr($normalized, 0, -2); - } - return $normalized; + $htmlPart = $this->getHtmlPart($index); + if ($htmlPart !== null) { + return $htmlPart->getContentResourceHandle($charset); } return null; } - - /** - * Enforces the message to be a mime message for a non-mime (e.g. uuencoded - * or unspecified) message. If the message has uuencoded attachments, sets - * up the message as a multipart/mixed message and creates a content part. - */ - private function enforceMime() - { - if (!$this->isMime()) { - if ($this->getAttachmentCount()) { - $this->setMessageAsMixed(); - } else { - $this->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"us-ascii\""); - } - $this->setRawHeader('Mime-Version', '1.0'); - } - } - - /** - * Creates a multipart/related part out of 'inline' children of $parent and - * returns it. - * - * @param MimePart $parent - * @return MimePart - */ - private function createMultipartRelatedPartForInlineChildrenOf(MimePart $parent) - { - $relatedPart = $this->mimePartFactory->newMimePart(); - $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related'); - foreach ($parent->getChildParts(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) as $part) { - $this->removePart($part); - $relatedPart->addPart($part); - } - $parent->addPart($relatedPart, 0); - return $relatedPart; - } /** - * Finds an alternative inline part in the message and returns it if one - * exists. - * - * If the passed $mimeType is text/plain, searches for a text/html part. - * Otherwise searches for a text/plain part to return. + * Returns the content of the inline text/html part at the given index. * - * @param string $mimeType - * @return MimeType or null if not found - */ - private function findOtherContentPartFor($mimeType) - { - $altPart = $this->getPart( - 0, - PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain') - ); - if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) { - $altPartParent = $altPart->getParent(); - if ($altPartParent->getPartCount(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) !== 1) { - $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent); - } - } - return $altPart; - } - - /** - * Creates a new content part for the passed mimeType and charset, making - * space by creating a multipart/alternative if needed + * Reads the entire stream content into a string and returns it. Returns + * null if the message doesn't have an inline html part. * - * @param string $mimeType + * @param int $index * @param string $charset - * @return \ZBateson\MailMimeParser\Message\MimePart + * @return string */ - private function createContentPartForMimeType($mimeType, $charset) + public function getHtmlContent($index = 0, $charset = MailMimeParser::DEFAULT_CHARSET) { - $mimePart = $this->mimePartFactory->newMimePart(); - $mimePart->setRawHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$charset\""); - $mimePart->setRawHeader('Content-Transfer-Encoding', 'quoted-printable'); - $this->enforceMime(); - - $altPart = $this->findOtherContentPartFor($mimeType); - - if ($altPart === $this) { - $this->setMessageAsAlternative(); - $this->addPart($mimePart); - } elseif ($altPart !== null) { - $mimeAltPart = $this->createAlternativeContentPart($altPart); - $mimeAltPart->addPart($mimePart, 1); - } else { - $this->addPart($mimePart, 0); + $part = $this->getHtmlPart($index); + if ($part !== null) { + return $part->getContent($charset); } - - return $mimePart; + return null; } - + /** - * Either creates a mime part or sets the existing mime part with the passed - * mimeType to $strongOrHandle. + * Returns true if either a Content-Type or Mime-Version header are defined + * in this Message. * - * @param string $mimeType - * @param string|resource $stringOrHandle - * @param string $charset + * @return bool */ - protected function setContentPartForMimeType($mimeType, $stringOrHandle, $charset) + public function isMime() { - $part = ($mimeType === 'text/html') ? $this->getHtmlPart() : $this->getTextPart(); - $handle = $this->getHandleForStringOrHandle($stringOrHandle); - if ($part === null) { - $part = $this->createContentPartForMimeType($mimeType, $charset); - } else { - $contentType = $part->getHeaderValue('Content-Type', 'text/plain'); - $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$charset\""); - } - $part->attachContentResourceHandle($handle); + $contentType = $this->getHeaderValue('Content-Type'); + $mimeVersion = $this->getHeaderValue('Mime-Version'); + return ($contentType !== null || $mimeVersion !== null); } - + /** * Sets the text/plain part of the message to the passed $stringOrHandle, * either creating a new part if one doesn't exist for text/plain, or * assigning the value of $stringOrHandle to an existing text/plain part. - * + * * The optional $charset parameter is the charset for saving to. * $stringOrHandle is expected to be in UTF-8 regardless of the target * charset. - * + * * @param string|resource $stringOrHandle * @param string $charset */ public function setTextPart($stringOrHandle, $charset = 'UTF-8') { - $this->setContentPartForMimeType('text/plain', $stringOrHandle, $charset); + $this->messageHelperService + ->getMultipartHelper() + ->setContentPartForMimeType( + $this, 'text/plain', $stringOrHandle, $charset + ); } - + /** * Sets the text/html part of the message to the passed $stringOrHandle, * either creating a new part if one doesn't exist for text/html, or * assigning the value of $stringOrHandle to an existing text/html part. - * + * * The optional $charset parameter is the charset for saving to. * $stringOrHandle is expected to be in UTF-8 regardless of the target * charset. - * + * * @param string|resource $stringOrHandle * @param string $charset */ public function setHtmlPart($stringOrHandle, $charset = 'UTF-8') { - $this->setContentPartForMimeType('text/html', $stringOrHandle, $charset); + $this->messageHelperService + ->getMultipartHelper() + ->setContentPartForMimeType( + $this, 'text/html', $stringOrHandle, $charset + ); } - + /** * Removes the text/plain part of the message at the passed index if one * exists. Returns true on success. - * + * * @return bool true on success */ public function removeTextPart($index = 0) { - return $this->removePartByMimeType('text/plain', $index); + return $this->messageHelperService + ->getMultipartHelper() + ->removePartByMimeType( + $this, 'text/plain', $index + ); } /** * Removes all text/plain inline parts in this message, optionally keeping * other inline parts as attachments on the main message (defaults to * keeping them). - * + * * @param bool $keepOtherPartsAsAttachments * @return bool true on success */ public function removeAllTextParts($keepOtherPartsAsAttachments = true) { - return $this->removeAllContentPartsByMimeType('text/plain', $keepOtherPartsAsAttachments); + return $this->messageHelperService + ->getMultipartHelper() + ->removeAllContentPartsByMimeType( + $this, 'text/plain', $keepOtherPartsAsAttachments + ); } - + /** * Removes the html part of the message if one exists. Returns true on * success. - * + * * @return bool true on success */ public function removeHtmlPart($index = 0) { - return $this->removePartByMimeType('text/html', $index); + return $this->messageHelperService + ->getMultipartHelper() + ->removePartByMimeType( + $this, 'text/html', $index + ); } - + /** * Removes all text/html inline parts in this message, optionally keeping * other inline parts as attachments on the main message (defaults to * keeping them). - * + * * @param bool $keepOtherPartsAsAttachments * @return bool true on success */ public function removeAllHtmlParts($keepOtherPartsAsAttachments = true) { - return $this->removeAllContentPartsByMimeType('text/html', $keepOtherPartsAsAttachments); - } - - /** - * Returns the attachment part at the given 0-based index, or null if none - * is set. - * - * @param int $index - * @return \ZBateson\MailMimeParser\Message\MimePart - */ - public function getAttachmentPart($index) - { - $attachments = $this->getAllAttachmentParts(); - if (!isset($attachments[$index])) { - return null; - } - return $attachments[$index]; - } - - /** - * Returns all attachment parts. - * - * Attachments are any non-multipart, non-signature and non inline text or - * html part (a text or html part with a Content-Disposition set to - * 'attachment' is considered an attachment). - * - * @return \ZBateson\MailMimeParser\Message\MimePart[] - */ - public function getAllAttachmentParts() - { - $parts = $this->getAllParts( - new PartFilter([ - 'multipart' => PartFilter::FILTER_EXCLUDE - ]) - ); - return array_values(array_filter( - $parts, - function ($part) { - return !( - $part->isTextPart() - && $part->getHeaderValue('Content-Disposition', 'inline') === 'inline' - ); - } - )); - } - - /** - * Returns the number of attachments available. - * - * @return int - */ - public function getAttachmentCount() - { - return count($this->getAllAttachmentParts()); + return $this->messageHelperService + ->getMultipartHelper() + ->removeAllContentPartsByMimeType( + $this, 'text/html', $keepOtherPartsAsAttachments + ); } - + /** * Removes the attachment with the given index - * + * * @param int $index */ public function removeAttachmentPart($index) @@ -922,60 +382,37 @@ public function removeAttachmentPart($index) $part = $this->getAttachmentPart($index); $this->removePart($part); } - - /** - * Creates and returns a MimePart for use with a new attachment part being - * created. - * - * @return \ZBateson\MailMimeParser\Message\MimePart - */ - protected function createPartForAttachment() - { - if ($this->isMime()) { - $part = $this->mimePartFactory->newMimePart(); - $part->setRawHeader('Content-Transfer-Encoding', 'base64'); - if ($this->getHeaderValue('Content-Type') !== 'multipart/mixed') { - $this->setMessageAsMixed(); - } - return $part; - } - return $this->mimePartFactory->newUUEncodedPart(); - } - + /** * Adds an attachment part for the passed raw data string or handle and * given parameters. - * + * * @param string|handle $stringOrHandle * @param strubg $mimeType * @param string $filename * @param string $disposition - * @return \ZBateson\MailMimeParser\Message\MimePart */ public function addAttachmentPart($stringOrHandle, $mimeType, $filename = null, $disposition = 'attachment') { if ($filename === null) { $filename = 'file' . uniqid(); } - $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename); - $part = $this->createPartForAttachment(); - $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\""); - $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\""); - $part->attachContentResourceHandle($this->getHandleForStringOrHandle($stringOrHandle)); - $this->addPart($part); - return $part; + $part = $this->messageHelperService + ->getMultipartHelper() + ->createPartForAttachment($this, $mimeType, $filename, $disposition); + $part->setContent($stringOrHandle); + $this->addChild($part); } - + /** * Adds an attachment part using the passed file. - * + * * Essentially creates a file stream and uses it. - * + * * @param string $file * @param string $mimeType * @param string $filename * @param string $disposition - * @return \ZBateson\MailMimeParser\Message\MimePart */ public function addAttachmentPartFromFile($file, $mimeType, $filename = null, $disposition = 'attachment') { @@ -983,129 +420,85 @@ public function addAttachmentPartFromFile($file, $mimeType, $filename = null, $d if ($filename === null) { $filename = basename($file); } - $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename); - $part = $this->createPartForAttachment(); - $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\""); - $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\""); - $part->attachContentResourceHandle($handle); - $this->addPart($part); - return $part; - } - - /** - * Returns a resource handle where the 'inline' text/plain content at the - * passed $index can be read or null if unavailable. - * - * @param int $index - * @return resource - */ - public function getTextStream($index = 0) - { - $textPart = $this->getTextPart($index); - if ($textPart !== null) { - return $textPart->getContentResourceHandle(); - } - return null; + $this->addAttachmentPart($handle, $mimeType, $filename, $disposition); } - + /** - * Returns the content of the inline text/plain part at the given index. - * - * Reads the entire stream content into a string and returns it. Returns - * null if the message doesn't have an inline text part. - * - * @param int $index - * @return string + * Returns a string containing the entire body of a signed message for + * verification or calculating a signature. + * + * @return string or null if the message doesn't have any children, or the + * child returns null for getHandle */ - public function getTextContent($index = 0) + public function getSignedMessageAsString() { - $part = $this->getTextPart($index); - if ($part !== null) { - return $part->getContent(); + $child = $this->getChild(0); + if ($child !== null) { + $normalized = preg_replace( + '/\r\n|\r|\n/', + "\r\n", + $child->getStream()->getContents() + ); + return $normalized; } return null; } - + /** - * Returns a resource handle where the 'inline' text/html content at the - * passed $index can be read or null if unavailable. - * - * @return resource + * Returns the signature part of a multipart/signed message or null. + * + * The signature part is determined to always be the 2nd child of a + * multipart/signed message, the first being the 'body'. + * + * Using the 'protocol' parameter of the Content-Type header is unreliable + * in some instances (for instance a difference of x-pgp-signature versus + * pgp-signature). + * + * @return MimePart */ - public function getHtmlStream($index = 0) + public function getSignaturePart() { - $htmlPart = $this->getHtmlPart($index); - if ($htmlPart !== null) { - return $htmlPart->getContentResourceHandle(); + $contentType = $this->getHeaderValue('Content-Type', 'text/plain'); + if (strcasecmp($contentType, 'multipart/signed') === 0) { + return $this->getChild(1); + } else { + return null; } - return null; } - + /** - * Returns the content of the inline text/html part at the given index. - * - * Reads the entire stream content into a string and returns it. Returns - * null if the message doesn't have an inline html part. - * - * @param int $index - * @return string + * Turns the message into a multipart/signed message, moving the actual + * message into a child part, sets the content-type of the main message to + * multipart/signed and adds an empty signature part as well. + * + * After calling setAsMultipartSigned, call get + * + * @param string $micalg The Message Integrity Check algorithm being used + * @param string $protocol The mime-type of the signature body */ - public function getHtmlContent($index = 0) + public function setAsMultipartSigned($micalg, $protocol) { - $part = $this->getHtmlPart($index); - if ($part !== null) { - return $part->getContent(); + $contentType = $this->getHeaderValue('Content-Type', 'text/plain'); + if (strcasecmp($contentType, 'multipart/signed') !== 0) { + $this->messageHelperService->getPrivacyHelper() + ->setMessageAsMultipartSigned($this, $micalg, $protocol); } - return null; + $this->messageHelperService->getPrivacyHelper() + ->overwrite8bitContentEncoding($this); + $this->messageHelperService->getPrivacyHelper() + ->ensureHtmlPartFirstForSignedMessage($this); + $this->setSignature('Not set'); } - - /** - * Returns true if either a Content-Type or Mime-Version header are defined - * in this Message. - * - * @return bool - */ - public function isMime() - { - $contentType = $this->getHeaderValue('Content-Type'); - $mimeVersion = $this->getHeaderValue('Mime-Version'); - return ($contentType !== null || $mimeVersion !== null); - } - - /** - * Saves the message as a MIME message to the passed resource handle. - * - * @param resource $handle - */ - public function save($handle) - { - $this->messageWriter->writeMessageTo($this, $handle); - } - - /** - * Returns the content part of a signed message for a signature to be - * calculated on the message. - * - * @return string - */ - public function getSignableBody() - { - return $this->messageWriter->getSignableBody($this); - } - + /** - * Shortcut to call Message::save with a php://temp stream and return the - * written email message as a string. - * - * @return string + * Sets the signature body of the message to the passed $body for a + * multipart/signed message. + * + * @param string $body */ - public function __toString() + public function setSignature($body) { - $handle = fopen('php://temp', 'r+'); - $this->save($handle); - rewind($handle); - $str = stream_get_contents($handle); - fclose($handle); - return $str; + $this->messageHelperService->getPrivacyHelper() + ->setSignature($this, $body); } } diff --git a/src/Message/Helper/AbstractHelper.php b/src/Message/Helper/AbstractHelper.php new file mode 100644 index 00000000..d047478f --- /dev/null +++ b/src/Message/Helper/AbstractHelper.php @@ -0,0 +1,49 @@ +mimePartFactory = $mimePartFactory; + $this->uuEncodedPartFactory = $uuEncodedPartFactory; + $this->partBuilderFactory = $partBuilderFactory; + } +} diff --git a/src/Message/Helper/GenericHelper.php b/src/Message/Helper/GenericHelper.php new file mode 100644 index 00000000..4c9221a8 --- /dev/null +++ b/src/Message/Helper/GenericHelper.php @@ -0,0 +1,139 @@ +getHeader($header); + $set = ($fromHeader !== null) ? $fromHeader->getRawValue() : $default; + if ($set !== null) { + $to->setRawHeader($header, $set); + } + } + + /** + * Removes the following headers from the passed part: Content-Type, + * Content-Transfer-Encoding, Content-Disposition, Content-ID and + * Content-Description, then detaches its content stream. + * + * @param ParentHeaderPart $part + */ + public function removeTypeHeadersAndContent(ParentHeaderPart $part) + { + $part->removeHeader('Content-Type'); + $part->removeHeader('Content-Transfer-Encoding'); + $part->removeHeader('Content-Disposition'); + $part->removeHeader('Content-ID'); + $part->removeHeader('Content-Description'); + $part->detachContentStream(); + } + + /** + * Copies Content-Type, Content-Disposition and Content-Transfer-Encoding + * headers from the $from header into the $to header. If the Content-Type + * header isn't defined in $from, defaults to text/plain with utf-8 and + * quoted-printable. + * + * @param ParentHeaderPart $from + * @param ParentHeaderPart $to + */ + public function copyTypeHeadersAndContent(ParentHeaderPart $from, ParentHeaderPart $to, $move = false) + { + $this->copyHeader($from, $to, 'Content-Type', 'text/plain; charset=utf-8'); + if ($from->getHeader('Content-Type') === null) { + $this->copyHeader($from, $to, 'Content-Transfer-Encoding', 'quoted-printable'); + } else { + $this->copyHeader($from, $to, 'Content-Transfer-Encoding'); + } + $this->copyHeader($from, $to, 'Content-Disposition'); + $this->copyHeader($from, $to, 'Content-ID'); + $this->copyHeader($from, $to, 'Content-Description'); + if ($from->hasContent()) { + $to->attachContentStream($from->getContentStream(), MailMimeParser::DEFAULT_CHARSET); + } + if ($move) { + $this->removeTypeHeadersAndContent($from); + } + } + + /** + * Creates a new content part from the passed part, allowing the part to be + * used for something else (e.g. changing a non-mime message to a multipart + * mime message). + * + * @param ParentHeaderPart $part + * @return MimePart the newly-created MimePart + */ + public function createNewContentPartFrom(ParentHeaderPart $part) + { + $mime = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart(); + $this->copyTypeHeadersAndContent($part, $mime, true); + return $mime; + } + + /** + * Copies type headers (Content-Type, Content-Disposition, + * Content-Transfer-Encoding) from the $from MimePart to $to. Attaches the + * content resource handle of $from to $to, and loops over child parts, + * removing them from $from and adding them to $to. + * + * @param ParentHeaderPart $from + * @param ParentHeaderPart $to + */ + public function movePartContentAndChildren(ParentHeaderPart $from, ParentHeaderPart $to) + { + $this->copyTypeHeadersAndContent($from, $to, true); + foreach ($from->getChildParts() as $child) { + $from->removePart($child); + $to->addChild($child); + } + } + + /** + * Replaces the $part ParentHeaderPart with $replacement. + * + * Essentially removes $part from its parent, and adds $replacement in its + * same position. If $part is this Message, its type headers are moved from + * this message to $replacement, the content resource is moved, and children + * are assigned to $replacement. + * + * @param ParentHeaderPart $part + * @param ParentHeaderPart $replacement + */ + public function replacePart(Message $message, ParentHeaderPart $part, ParentHeaderPart $replacement) + { + $message->removePart($replacement); + if ($part === $message) { + $this->movePartContentAndChildren($replacement, $part); + return; + } + $parent = $part->getParent(); + $position = $parent->removePart($part); + $parent->addChild($replacement, $position); + } +} diff --git a/src/Message/Helper/MessageHelperService.php b/src/Message/Helper/MessageHelperService.php new file mode 100644 index 00000000..6623afdd --- /dev/null +++ b/src/Message/Helper/MessageHelperService.php @@ -0,0 +1,116 @@ +partBuilderFactory = $partBuilderFactory; + } + + /** + * Set separately to avoid circular dependencies (PartFactoryService needs a + * MessageHelperService). + * + * @param PartFactoryService $partFactoryService + */ + public function setPartFactoryService(PartFactoryService $partFactoryService) + { + $this->partFactoryService = $partFactoryService; + } + + /** + * Returns the GenericHelper singleton + * + * @return GenericHelper + */ + public function getGenericHelper() + { + if ($this->genericHelper === null) { + $this->genericHelper = new GenericHelper( + $this->partFactoryService->getMimePartFactory(), + $this->partFactoryService->getUUEncodedPartFactory(), + $this->partBuilderFactory + ); + } + return $this->genericHelper; + } + + /** + * Returns the MultipartHelper singleton + * + * @return MultipartHelper + */ + public function getMultipartHelper() + { + if ($this->multipartHelper === null) { + $this->multipartHelper = new MultipartHelper( + $this->partFactoryService->getMimePartFactory(), + $this->partFactoryService->getUUEncodedPartFactory(), + $this->partBuilderFactory, + $this->getGenericHelper() + ); + } + return $this->multipartHelper; + } + + /** + * Returns the PrivacyHelper singleton + * + * @return PrivacyHelper + */ + public function getPrivacyHelper() + { + if ($this->privacyHelper === null) { + $this->privacyHelper = new PrivacyHelper( + $this->partFactoryService->getMimePartFactory(), + $this->partFactoryService->getUUEncodedPartFactory(), + $this->partBuilderFactory, + $this->getGenericHelper(), + $this->getMultipartHelper() + ); + } + return $this->privacyHelper; + } +} diff --git a/src/Message/Helper/MultipartHelper.php b/src/Message/Helper/MultipartHelper.php new file mode 100644 index 00000000..1a17e683 --- /dev/null +++ b/src/Message/Helper/MultipartHelper.php @@ -0,0 +1,430 @@ +genericHelper = $genericHelper; + } + + /** + * Creates and returns a unique boundary. + * + * @param string $mimeType first 3 characters of a multipart type are used, + * e.g. REL for relative or ALT for alternative + * @return string + */ + public function getUniqueBoundary($mimeType) + { + $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-'); + return uniqid('----=MMP-' . $type . '.', true); + } + + /** + * Creates a unique mime boundary and assigns it to the passed part's + * Content-Type header with the passed mime type. + * + * @param ParentHeaderPart $part + * @param string $mimeType + */ + public function setMimeHeaderBoundaryOnPart(ParentHeaderPart $part, $mimeType) + { + $part->setRawHeader( + 'Content-Type', + "$mimeType;\r\n\tboundary=\"" + . $this->getUniqueBoundary($mimeType) . '"' + ); + } + + /** + * Sets the passed message as multipart/mixed. + * + * If the message has content, a new part is created and added as a child of + * the message. The message's content and content headers are moved to the + * new part. + * + * @param Message $message + */ + public function setMessageAsMixed(Message $message) + { + if ($message->hasContent()) { + $part = $this->genericHelper->createNewContentPartFrom($message); + $message->addChild($part, 0); + } + $this->setMimeHeaderBoundaryOnPart($message, 'multipart/mixed'); + $atts = $message->getAllAttachmentParts(); + if (!empty($atts)) { + foreach ($atts as $att) { + $att->markAsChanged(); + } + } + } + + /** + * Sets the passed message as multipart/alternative. + * + * If the message has content, a new part is created and added as a child of + * the message. The message's content and content headers are moved to the + * new part. + * + * @param Message $message + */ + public function setMessageAsAlternative(Message $message) + { + if ($message->hasContent()) { + $part = $this->genericHelper->createNewContentPartFrom($message); + $message->addChild($part, 0); + } + $this->setMimeHeaderBoundaryOnPart($message, 'multipart/alternative'); + } + + /** + * Searches the passed $alternativePart for a part with the passed mime type + * and returns its parent. + * + * Used for alternative mime types that have a multipart/mixed or + * multipart/related child containing a content part of $mimeType, where + * the whole mixed/related part should be removed. + * + * @param string $mimeType the content-type to find below $alternativePart + * @param ParentHeaderPart $alternativePart The multipart/alternative part to look + * under + * @return boolean|MimePart false if a part is not found + */ + public function getContentPartContainerFromAlternative($mimeType, ParentHeaderPart $alternativePart) + { + $part = $alternativePart->getPart(0, PartFilter::fromInlineContentType($mimeType)); + $contPart = null; + do { + if ($part === null) { + return false; + } + $contPart = $part; + $part = $part->getParent(); + } while ($part !== $alternativePart); + return $contPart; + } + + /** + * Removes all parts of $mimeType from $alternativePart. + * + * If $alternativePart contains a multipart/mixed or multipart/relative part + * with other parts of different content-types, the multipart part is + * removed, and parts of different content-types can optionally be moved to + * the main message part. + * + * @param Message $message + * @param string $mimeType + * @param ParentHeaderPart $alternativePart + * @param bool $keepOtherContent + * @return bool + */ + public function removeAllContentPartsFromAlternative(Message $message, $mimeType, ParentHeaderPart $alternativePart, $keepOtherContent) + { + $rmPart = $this->getContentPartContainerFromAlternative($mimeType, $alternativePart); + if ($rmPart === false) { + return false; + } + if ($keepOtherContent) { + $this->moveAllPartsAsAttachmentsExcept($message, $rmPart, $mimeType); + $alternativePart = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative')); + } else { + $rmPart->removeAllParts(); + } + $message->removePart($rmPart); + if ($alternativePart !== null) { + if ($alternativePart->getChildCount() === 1) { + $this->genericHelper->replacePart($message, $alternativePart, $alternativePart->getChild(0)); + } elseif ($alternativePart->getChildCount() === 0) { + $message->removePart($alternativePart); + } + } + while ($message->getChildCount() === 1) { + $this->genericHelper->replacePart($message, $message, $message->getChild(0)); + } + return true; + } + + /** + * Creates a new mime part as a multipart/alternative and assigns the passed + * $contentPart as a part below it before returning it. + * + * @param Message $message + * @param MessagePart $contentPart + * @return MimePart the alternative part + */ + public function createAlternativeContentPart(Message $message, MessagePart $contentPart) + { + $altPart = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart(); + $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative'); + $message->removePart($contentPart); + $message->addChild($altPart, 0); + $altPart->addChild($contentPart, 0); + return $altPart; + } + + /** + * Moves all parts under $from into this message except those with a + * content-type equal to $exceptMimeType. If the message is not a + * multipart/mixed message, it is set to multipart/mixed first. + * + * @param Message $message + * @param ParentHeaderPart $from + * @param string $exceptMimeType + */ + public function moveAllPartsAsAttachmentsExcept(Message $message, ParentHeaderPart $from, $exceptMimeType) + { + $parts = $from->getAllParts(new PartFilter([ + 'multipart' => PartFilter::FILTER_EXCLUDE, + 'headers' => [ + PartFilter::FILTER_EXCLUDE => [ + 'Content-Type' => $exceptMimeType + ] + ] + ])); + if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) { + $this->setMessageAsMixed($message); + } + foreach ($parts as $part) { + $from->removePart($part); + $message->addChild($part); + } + } + + /** + * Enforces the message to be a mime message for a non-mime (e.g. uuencoded + * or unspecified) message. If the message has uuencoded attachments, sets + * up the message as a multipart/mixed message and creates a separate + * content part. + * + * @param Message $message + */ + public function enforceMime(Message $message) + { + if (!$message->isMime()) { + if ($message->getAttachmentCount()) { + $this->setMessageAsMixed($message); + } else { + $message->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"iso-8859-1\""); + } + $message->setRawHeader('Mime-Version', '1.0'); + } + } + + /** + * Creates a multipart/related part out of 'inline' children of $parent and + * returns it. + * + * @param ParentHeaderPart $parent + * @return MimePart + */ + public function createMultipartRelatedPartForInlineChildrenOf(ParentHeaderPart $parent) + { + $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory); + $relatedPart = $builder->createMessagePart(); + $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related'); + foreach ($parent->getChildParts(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) as $part) { + $parent->removePart($part); + $relatedPart->addChild($part); + } + $parent->addChild($relatedPart, 0); + return $relatedPart; + } + + /** + * Finds an alternative inline part in the message and returns it if one + * exists. + * + * If the passed $mimeType is text/plain, searches for a text/html part. + * Otherwise searches for a text/plain part to return. + * + * @param Message $message + * @param string $mimeType + * @return MimeType or null if not found + */ + public function findOtherContentPartFor(Message $message, $mimeType) + { + $altPart = $message->getPart( + 0, + PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain') + ); + if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) { + $altPartParent = $altPart->getParent(); + if ($altPartParent->getPartCount(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) !== 1) { + $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent); + } + } + return $altPart; + } + + /** + * Creates a new content part for the passed mimeType and charset, making + * space by creating a multipart/alternative if needed + * + * @param Message $message + * @param string $mimeType + * @param string $charset + * @return MimePart + */ + public function createContentPartForMimeType(Message $message, $mimeType, $charset) + { + $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory); + $builder->addHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$charset\""); + $builder->addHeader('Content-Transfer-Encoding', 'quoted-printable'); + $this->enforceMime($message); + $mimePart = $builder->createMessagePart(); + + $altPart = $this->findOtherContentPartFor($message, $mimeType); + + if ($altPart === $message) { + $this->setMessageAsAlternative($message); + $message->addChild($mimePart); + } elseif ($altPart !== null) { + $mimeAltPart = $this->createAlternativeContentPart($message, $altPart); + $mimeAltPart->addChild($mimePart, 1); + } else { + $message->addChild($mimePart, 0); + } + + return $mimePart; + } + + /** + * Creates and returns a MimePart for use with a new attachment part being + * created. + * + * @param Message $message + * @param string $mimeType + * @param string $filename + * @param string $disposition + * @return MimePart + */ + public function createPartForAttachment(Message $message, $mimeType, $filename, $disposition) + { + $safe = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename); + if ($message->isMime()) { + $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory); + $builder->addHeader('Content-Transfer-Encoding', 'base64'); + if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) { + $this->setMessageAsMixed($message); + } + $builder->addHeader('Content-Type', "$mimeType;\r\n\tname=\"$safe\""); + $builder->addHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$safe\""); + } else { + $builder = $this->partBuilderFactory->newPartBuilder( + $this->uuEncodedPartFactory + ); + $builder->setProperty('filename', $safe); + } + return $builder->createMessagePart(); + } + + /** + * Removes the content part of the message with the passed mime type. If + * there is a remaining content part and it is an alternative part of the + * main message, the content part is moved to the message part. + * + * If the content part is part of an alternative part beneath the message, + * the alternative part is replaced by the remaining content part, + * optionally keeping other parts if $keepOtherContent is set to true. + * + * @param Message $message + * @param string $mimeType + * @param bool $keepOtherContent + * @return boolean true on success + */ + public function removeAllContentPartsByMimeType(Message $message, $mimeType, $keepOtherContent = false) + { + $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative')); + if ($alt !== null) { + return $this->removeAllContentPartsFromAlternative($message, $mimeType, $alt, $keepOtherContent); + } + $message->removeAllParts(PartFilter::fromInlineContentType($mimeType)); + return true; + } + + /** + * Removes the 'inline' part with the passed contentType, at the given index + * defaulting to the first + * + * @param Message $message + * @param string $mimeType + * @param int $index + * @return boolean true on success + */ + public function removePartByMimeType(Message $message, $mimeType, $index = 0) + { + $parts = $message->getAllParts(PartFilter::fromInlineContentType($mimeType)); + $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative')); + if ($parts === null || !isset($parts[$index])) { + return false; + } elseif (count($parts) === 1) { + return $this->removeAllContentPartsByMimeType($message, $mimeType, true); + } + $part = $parts[$index]; + $message->removePart($part); + if ($alt !== null && $alt->getChildCount() === 1) { + $this->genericHelper->replacePart($message, $alt, $alt->getChild(0)); + } + return true; + } + + /** + * Either creates a mime part or sets the existing mime part with the passed + * mimeType to $strongOrHandle. + * + * @param Message $message + * @param string $mimeType + * @param string|resource $stringOrHandle + * @param string $charset + */ + public function setContentPartForMimeType(Message $message, $mimeType, $stringOrHandle, $charset) + { + $part = ($mimeType === 'text/html') ? $message->getHtmlPart() : $message->getTextPart(); + if ($part === null) { + $part = $this->createContentPartForMimeType($message, $mimeType, $charset); + } else { + $contentType = $part->getHeaderValue('Content-Type', 'text/plain'); + $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$charset\""); + } + $part->setContent($stringOrHandle); + } +} diff --git a/src/Message/Helper/PrivacyHelper.php b/src/Message/Helper/PrivacyHelper.php new file mode 100644 index 00000000..9bace02c --- /dev/null +++ b/src/Message/Helper/PrivacyHelper.php @@ -0,0 +1,142 @@ +genericHelper = $genericHelper; + $this->multipartHelper = $multipartHelper; + } + + /** + * The passed message is set as multipart/signed, and a new part is created + * below it with content headers, content and children copied from the + * message. + * + * @param Message $message + * @param string $micalg + * @param string $protocol + */ + public function setMessageAsMultipartSigned(Message $message, $micalg, $protocol) + { + $this->multipartHelper->enforceMime($message); + $messagePart = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart(); + $this->genericHelper->movePartContentAndChildren($message, $messagePart); + $message->addChild($messagePart); + + $boundary = $this->multipartHelper->getUniqueBoundary('multipart/signed'); + $message->setRawHeader( + 'Content-Type', + "multipart/signed;\r\n\tboundary=\"$boundary\";\r\n\tmicalg=\"$micalg\"; protocol=\"$protocol\"" + ); + } + + /** + * Sets the signature of the message to $body, creating a signature part if + * one doesn't exist. + * + * @param Message $message + * @param string $body + */ + public function setSignature(Message $message, $body) + { + $signedPart = $message->getSignaturePart(); + if ($signedPart === null) { + $signedPart = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart(); + $message->addChild($signedPart); + } + $signedPart->setRawHeader( + 'Content-Type', + $message->getHeaderParameter('Content-Type', 'protocol') + ); + $signedPart->setContent($body); + } + + /** + * Loops over parts of the message and sets the content-transfer-encoding + * header to quoted-printable for text/* mime parts, and to base64 + * otherwise for parts that are '8bit' encoded. + * + * Used for multipart/signed messages which doesn't support 8bit transfer + * encodings. + * + * @param Message $message + */ + public function overwrite8bitContentEncoding(Message $message) + { + $parts = $message->getAllParts(new PartFilter([ + 'headers' => [ PartFilter::FILTER_INCLUDE => [ + 'Content-Transfer-Encoding' => '8bit' + ] ] + ])); + foreach ($parts as $part) { + $contentType = strtolower($part->getHeaderValue('Content-Type', 'text/plain')); + if ($contentType === 'text/plain' || $contentType === 'text/html') { + $part->setRawHeader('Content-Transfer-Encoding', 'quoted-printable'); + } else { + $part->setRawHeader('Content-Transfer-Encoding', 'base64'); + } + } + } + + /** + * Ensures a non-text part comes first in a signed multipart/alternative + * message as some clients seem to prefer the first content part if the + * client doesn't understand multipart/signed. + * + * @param Message $message + */ + public function ensureHtmlPartFirstForSignedMessage(Message $message) + { + $alt = $message->getPartByMimeType('multipart/alternative'); + if ($alt !== null && $alt instanceof ParentPart) { + $cont = $this->multipartHelper->getContentPartContainerFromAlternative('text/html', $alt); + $children = $alt->getChildParts(); + $pos = array_search($cont, $children, true); + if ($pos !== false && $pos !== 0) { + $alt->removePart($children[0]); + $alt->addChild($children[0]); + } + } + } +} diff --git a/src/Message/MessageFactory.php b/src/Message/MessageFactory.php new file mode 100644 index 00000000..00a36ccd --- /dev/null +++ b/src/Message/MessageFactory.php @@ -0,0 +1,73 @@ +messageHelperService = $mhs; + } + + /** + * Constructs a new Message object and returns it + * + * @param PartBuilder $partBuilder + * @param StreamInterface $stream + * @return \ZBateson\MailMimeParser\Message\Part\MimePart + */ + public function newInstance(PartBuilder $partBuilder, StreamInterface $stream = null) + { + $contentStream = null; + if ($stream !== null) { + $contentStream = $this->streamFactory->getLimitedContentStream($stream, $partBuilder); + } + return new Message( + $this->partStreamFilterManagerFactory->newInstance(), + $this->streamFactory, + $this->partFilterFactory, + $this->headerFactory, + $partBuilder, + $this->messageHelperService, + $stream, + $contentStream + ); + } +} diff --git a/src/Message/MessageParser.php b/src/Message/MessageParser.php index 5c80f46f..3922454e 100644 --- a/src/Message/MessageParser.php +++ b/src/Message/MessageParser.php @@ -6,8 +6,11 @@ */ namespace ZBateson\MailMimeParser\Message; -use ZBateson\MailMimeParser\Message; -use ZBateson\MailMimeParser\Stream\PartStreamRegistry; +use Psr\Http\Message\StreamInterface; +use ZBateson\MailMimeParser\Message\Part\PartBuilder; +use ZBateson\MailMimeParser\Message\Part\Factory\PartBuilderFactory; +use ZBateson\MailMimeParser\Message\Part\Factory\PartFactoryService; +use GuzzleHttp\Psr7\StreamWrapper; /** * Parses a mail mime message into its component parts. To invoke, call @@ -18,82 +21,80 @@ class MessageParser { /** - * @var \ZBateson\MailMimeParser\Message the Message object that the read - * mail mime message will be parsed into + * @var PartFactoryService service instance used to create MimePartFactory + * objects. */ - protected $message; + protected $partFactoryService; /** - * @var \ZBateson\MailMimeParser\Message\MimePartFactory the MimePartFactory object - * used to create parts. + * @var PartBuilderFactory used to create PartBuilders */ - protected $partFactory; + protected $partBuilderFactory; /** - * @var \ZBateson\MailMimeParser\Stream\PartStreamRegistry the - * PartStreamRegistry - * object used to register stream parts. + * @var int maintains the character length of the last line separator, + * typically 2 for CRLF, to keep track of the correct 'end' position + * for a part because the CRLF before a boundary is considered part of + * the boundary. */ - protected $partStreamRegistry; + private $lastLineSeparatorLength = 0; /** * Sets up the parser with its dependencies. * - * @param \ZBateson\MailMimeParser\Message $m - * @param \ZBateson\MailMimeParser\Message\MimePartFactory $pf - * @param \ZBateson\MailMimeParser\Stream\PartStreamRegistry $psr + * @param PartFactoryService $pfs + * @param PartBuilderFactory $pbf */ - public function __construct(Message $m, MimePartFactory $pf, PartStreamRegistry $psr) - { - $this->message = $m; - $this->partFactory = $pf; - $this->partStreamRegistry = $psr; + public function __construct( + PartFactoryService $pfs, + PartBuilderFactory $pbf + ) { + $this->partFactoryService = $pfs; + $this->partBuilderFactory = $pbf; } /** - * Parses the passed stream handle into the ZBateson\MailMimeParser\Message - * object and returns it. + * Parses the passed stream into a ZBateson\MailMimeParser\Message object + * and returns it. * - * @param resource $fhandle the resource handle to the input stream of the - * mime message + * @param StreamInterface $stream the stream to parse the message from * @return \ZBateson\MailMimeParser\Message */ - public function parse($fhandle) + public function parse(StreamInterface $stream) { - $this->partStreamRegistry->register($this->message->getObjectId(), $fhandle); - $this->read($fhandle, $this->message); - return $this->message; + $partBuilder = $this->read($stream); + return $partBuilder->createMessagePart($stream); } /** - * Ensures the header isn't empty, and contains a colon character, then - * splits it and assigns it to $part + * Ensures the header isn't empty and contains a colon separator character, + * then splits it and calls $partBuilder->addHeader. * * @param string $header - * @param \ZBateson\MailMimeParser\Message\MimePart $part + * @param PartBuilder $partBuilder */ - private function addRawHeaderToPart($header, MimePart $part) + private function addRawHeaderToPart($header, PartBuilder $partBuilder) { if ($header !== '' && strpos($header, ':') !== false) { $a = explode(':', $header, 2); - $part->setRawHeader($a[0], trim($a[1])); + $partBuilder->addHeader($a[0], trim($a[1])); } } /** - * Reads header lines up to an empty line, adding them to the passed $part. + * Reads header lines up to an empty line, adding them to the passed + * $partBuilder. * * @param resource $handle the resource handle to read from - * @param \ZBateson\MailMimeParser\Message\MimePart $part the current part to add - * headers to + * @param PartBuilder $partBuilder the current part to add headers to */ - protected function readHeaders($handle, MimePart $part) + protected function readHeaders($handle, PartBuilder $partBuilder) { $header = ''; do { $line = fgets($handle, 1000); - if ($line[0] !== "\t" && $line[0] !== ' ') { - $this->addRawHeaderToPart($header, $part); + if (empty($line) || $line[0] !== "\t" && $line[0] !== ' ') { + $this->addRawHeaderToPart($header, $partBuilder); $header = ''; } else { $line = "\r\n" . $line; @@ -103,275 +104,138 @@ protected function readHeaders($handle, MimePart $part) } /** - * Finds the end of the Mime part at the current read position in $handle - * and sets $boundaryLength to the number of bytes in the part, and - * $endBoundaryFound to true if it's an 'end' boundary, meaning there are no - * further parts for the current mime part (ends with --). + * Reads lines from the passed $handle, calling + * $partBuilder->setEndBoundaryFound with the passed line until it returns + * true or the stream is at EOF. + * + * setEndBoundaryFound returns true if the passed line matches a boundary + * for the $partBuilder itself or any of its parents. + * + * Once a boundary is found, setStreamPartAndContentEndPos is called with + * the passed $handle's read pos before the boundary and its line separator + * were read. * * @param resource $handle - * @param string $boundary - * @param int $boundaryLength - * @param boolean $endBoundaryFound + * @param PartBuilder $partBuilder */ - private function findPartBoundaries($handle, $boundary, &$boundaryLength, &$endBoundaryFound) + private function findContentBoundary($handle, PartBuilder $partBuilder) { - do { + // last separator before a boundary belongs to the boundary, and is not + // part of the current part + while (!feof($handle)) { + $endPos = ftell($handle) - $this->lastLineSeparatorLength; $line = fgets($handle); - $boundaryLength = strlen($line); - $test = rtrim($line); - if ($test === "--$boundary") { - break; - } elseif ($test === "--$boundary--") { - $endBoundaryFound = true; - break; + $test = rtrim($line, "\r\n"); + $this->lastLineSeparatorLength = strlen($line) - strlen($test); + if ($partBuilder->setEndBoundaryFound($test)) { + $partBuilder->setStreamPartAndContentEndPos($endPos); + return; } - } while (!feof($handle)); - } - - /** - * Adds the part to its parent. - * - * @param MimePart $part - */ - private function addToParent(MimePart $part) - { - if ($part->getParent() !== null) { - $part->getParent()->addPart($part); } + $partBuilder->setStreamPartAndContentEndPos(ftell($handle)); + $partBuilder->setEof(); } /** + * Reads content for a non-mime message. If there are uuencoded attachment + * parts in the message (denoted by 'begin' lines), those parts are read and + * added to the passed $partBuilder as children. * - * - * @param type $handle - * @param MimePart $part - * @param Message $message - * @param type $contentStartPos - * @param type $boundaryLength + * @param resource $handle + * @param PartBuilder $partBuilder + * @return string */ - protected function attachStreamHandles($handle, MimePart $part, Message $message, $contentStartPos, $boundaryLength) + protected function readUUEncodedOrPlainTextMessage($handle, PartBuilder $partBuilder) { - $end = ftell($handle) - $boundaryLength; - $this->partStreamRegistry->attachContentPartStreamHandle($part, $message, $contentStartPos, $end); - $this->partStreamRegistry->attachOriginalPartStreamHandle($part, $message, $part->startHandlePosition, $end); - - if ($part->getParent() !== null) { - do { - $end = ftell($handle); - } while (!feof($handle) && rtrim(fgets($handle)) === ''); - fseek($handle, $end, SEEK_SET); - $this->partStreamRegistry->attachOriginalPartStreamHandle( - $part->getParent(), - $message, - $part->getParent()->startHandlePosition, - $end - ); + $partBuilder->setStreamContentStartPos(ftell($handle)); + $part = $partBuilder; + while (!feof($handle)) { + $start = ftell($handle); + $line = trim(fgets($handle)); + if (preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) { + $part = $this->partBuilderFactory->newPartBuilder( + $this->partFactoryService->getUUEncodedPartFactory() + ); + $part->setStreamPartStartPos($start); + // 'begin' line is part of the content + $part->setStreamContentStartPos($start); + $part->setProperty('mode', $matches[1]); + $part->setProperty('filename', $matches[2]); + $partBuilder->addChild($part); + } + $part->setStreamPartAndContentEndPos(ftell($handle)); } + $partBuilder->setStreamPartEndPos(ftell($handle)); } /** - * Reads the content of a mime part up to a boundary, or the entire message - * if no boundary is specified. + * Reads content for a single part of a MIME message. * - * readPartContent may be called to skip to the first boundary to read its - * headers, in which case $skipPart should be true. + * If the part being read is in turn a multipart part, readPart is called on + * it recursively to read its headers and content. * - * If the end boundary is found, the method returns true. - * - * @param resource $handle the input stream resource - * @param \ZBateson\MailMimeParser\Message $message the current Message - * object - * @param \ZBateson\MailMimeParser\Message\MimePart $part the current MimePart - * object to load the content into. - * @param string $boundary the MIME boundary - * @param boolean $skipPart pass true if the intention is to read up to the - * beginning MIME boundary's headers - * @return boolean if the end boundary is found - */ - protected function readPartContent($handle, Message $message, MimePart $part, $boundary, $skipPart) - { - $start = ftell($handle); - $boundaryLength = 0; - $endBoundaryFound = false; - if ($boundary !== null) { - $this->findPartBoundaries($handle, $boundary, $boundaryLength, $endBoundaryFound); - } else { - fseek($handle, 0, SEEK_END); - } - $type = $part->getHeaderValue('Content-Type', 'text/plain'); - if (!$skipPart || preg_match('~multipart/\w+~i', $type)) { - $this->attachStreamHandles($handle, $part, $message, $start, $boundaryLength); - $this->addToParent($part); - } - return $endBoundaryFound; - } - - /** - * Returns the boundary from the parent MimePart, or the current boundary if - * $parent is null - * - * @param string $curBoundary - * @param \ZBateson\MailMimeParser\Message\MimePart $parent - * @return string - */ - private function getParentBoundary($curBoundary, MimePart $parent = null) - { - return $parent !== null ? - $parent->getHeaderParameter('Content-Type', 'boundary') : - $curBoundary; - } - - /** - * Instantiates and returns a new MimePart setting the part's parent to - * either the passed $parent, or $message if $parent is null. - * - * @param \ZBateson\MailMimeParser\Message $message - * @param \ZBateson\MailMimeParser\Message\MimePart $parent - * @return \ZBateson\MailMimeParser\Message\MimePart - */ - private function newMimePartForMessage(Message $message, MimePart $parent = null) - { - $nextPart = $this->partFactory->newMimePart(); - $nextPart->setParent($parent === null ? $message : $parent); - return $nextPart; - } - - /** - * Keeps reading if an end boundary is found, to find the parent's boundary - * and the part's content. + * The start/end positions of the part's content are set on the passed + * $partBuilder, which in turn sets the end position of the part and its + * parents. * * @param resource $handle - * @param \ZBateson\MailMimeParser\Message $message - * @param \ZBateson\MailMimeParser\Message\MimePart $parent - * @param \ZBateson\MailMimeParser\Message\MimePart $part - * @param string $boundary - * @param bool $skipFirst - * @return \ZBateson\MailMimeParser\Message\MimePart + * @param PartBuilder $partBuilder */ - private function readMimeMessageBoundaryParts( - $handle, - Message $message, - MimePart $parent, - MimePart $part, - $boundary, - $skipFirst - ) { - $skipPart = $skipFirst; - while ($this->readPartContent($handle, $message, $part, $boundary, $skipPart) && $parent !== null) { - $parent = $parent->getParent(); - // $boundary used by next call to readPartContent - $boundary = $this->getParentBoundary($boundary, $parent); - $skipPart = true; - } - return $this->newMimePartForMessage($message, $parent); - } - - /** - * Finds the boundaries for the current MimePart, reads its content and - * creates and returns the next part, setting its parent part accordingly. - * - * @param resource $handle The handle to read from - * @param \ZBateson\MailMimeParser\Message $message The current Message - * @param \ZBateson\MailMimeParser\Message\MimePart $part - * @return MimePart - */ - protected function readMimeMessagePart($handle, Message $message, MimePart $part) + private function readPartContent($handle, PartBuilder $partBuilder) { - $boundary = $part->getHeaderParameter('Content-Type', 'boundary'); - $skipFirst = true; - $parent = $part; - - if ($boundary === null || !$part->isMultiPart()) { - // either there is no boundary (possibly no parent boundary either) and message is read - // till the end, or we're in a boundary already and content should be read till the parent - // boundary is reached - if ($part->getParent() !== null) { - $parent = $part->getParent(); - $boundary = $parent->getHeaderParameter('Content-Type', 'boundary'); + $partBuilder->setStreamContentStartPos(ftell($handle)); + $this->findContentBoundary($handle, $partBuilder); + if ($partBuilder->isMultiPart()) { + while (!$partBuilder->isParentBoundaryFound()) { + $child = $this->partBuilderFactory->newPartBuilder( + $this->partFactoryService->getMimePartFactory() + ); + $partBuilder->addChild($child); + $this->readPart($handle, $child); } - $skipFirst = false; } - return $this->readMimeMessageBoundaryParts($handle, $message, $parent, $part, $boundary, $skipFirst); } /** - * Extracts the filename and end position of a UUEncoded part. - * - * The filename is set to the passed $nextFilename parameter. The end - * position is returned. - * - * @param resource $handle the current file handle - * @param int &$nextMode is assigned the value of the next file mode or null - * if not found - * @param string &$nextFilename is assigned the value of the next filename - * or null if not found - * @param int &$end assigned the offset position within the passed resource - * $handle of the end of the uuencoded part - */ - private function findNextUUEncodedPartPosition($handle) - { - $end = ftell($handle); - do { - $line = trim(fgets($handle)); - $matches = null; - if (preg_match('/^begin [0-7]{3} .*$/', $line, $matches)) { - fseek($handle, $end); - break; - } - $end = ftell($handle); - } while (!feof($handle)); - return $end; - } - - /** - * Reads one part of a UUEncoded message and adds it to the passed Message - * as a MimePart. - * - * The method reads up to the first 'begin' part of the message, or to the - * end of the message if no 'begin' exists. + * Reads a part and any of its children, into the passed $partBuilder, + * either by calling readUUEncodedOrPlainTextMessage or readPartContent + * after reading headers. * * @param resource $handle - * @param \ZBateson\MailMimeParser\Message $message - * @return string + * @param PartBuilder $partBuilder + * @param boolean $isMessage */ - protected function readUUEncodedOrPlainTextPart($handle, Message $message) + protected function readPart($handle, PartBuilder $partBuilder) { - $start = ftell($handle); - $line = trim(fgets($handle)); - $end = $this->findNextUUEncodedPartPosition($handle); - $part = $message; - if (preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) { - $mode = $matches[1]; - $filename = $matches[2]; - $part = $this->partFactory->newUUEncodedPart($mode, $filename); - $message->addPart($part); + $partBuilder->setStreamPartStartPos(ftell($handle)); + + if ($partBuilder->canHaveHeaders()) { + $this->readHeaders($handle, $partBuilder); + $this->lastLineSeparatorLength = 0; + } + if ($partBuilder->getParent() === null && !$partBuilder->isMime()) { + $this->readUUEncodedOrPlainTextMessage($handle, $partBuilder); + } else { + $this->readPartContent($handle, $partBuilder); } - $this->partStreamRegistry->attachContentPartStreamHandle($part, $message, $start, $end); } /** - * Reads the message from the input stream $handle into $message. - * - * The method will loop to read headers and find and parse multipart-mime - * message parts and uuencoded attachments (as mime-parts), adding them to - * the passed Message object. + * Reads the message from the passed stream and returns a PartBuilder + * representing it. * - * @param resource $handle - * @param \ZBateson\MailMimeParser\Message $message + * @param StreamInterface $stream + * @return PartBuilder */ - protected function read($handle, Message $message) + protected function read(StreamInterface $stream) { - $part = $message; - $part->startHandlePosition = 0; - $this->readHeaders($handle, $message); - do { - if (!$message->isMime()) { - $this->readUUEncodedOrPlainTextPart($handle, $message); - } else { - $part = $this->readMimeMessagePart($handle, $message, $part); - $part->startHandlePosition = ftell($handle); - $this->readHeaders($handle, $part); - } - } while (!feof($handle)); + $partBuilder = $this->partBuilderFactory->newPartBuilder( + $this->partFactoryService->getMessageFactory() + ); + // the remaining parts use a resource handle for better performance... + // it seems fgets does much better than Psr7\readline (not specifically + // measured, but difference in running tests is big) + $this->readPart(StreamWrapper::getResource($stream), $partBuilder); + return $partBuilder; } } diff --git a/src/Message/MimePart.php b/src/Message/MimePart.php deleted file mode 100644 index 7b8003be..00000000 --- a/src/Message/MimePart.php +++ /dev/null @@ -1,566 +0,0 @@ -headerFactory = $headerFactory; - $this->partWriter = $partWriter; - } - - /** - * Closes the attached resource handle. - */ - public function __destruct() - { - if (is_resource($this->handle)) { - fclose($this->handle); - } - if (is_resource($this->originalStreamHandle)) { - fclose($this->originalStreamHandle); - } - } - - /** - * Registers the passed part as a child of the current part. - * - * If the $position parameter is non-null, adds the part at the passed - * position index. - * - * @param \ZBateson\MailMimeParser\Message\MimePart $part - * @param int $position - */ - public function addPart(MimePart $part, $position = null) - { - if ($part !== $this) { - $part->setParent($this); - array_splice($this->parts, ($position === null) ? count($this->parts) : $position, 0, [ $part ]); - } - } - - /** - * Removes the child part from this part and returns its position or - * null if it wasn't found. - * - * Note that if the part is not a direct child of this part, the returned - * position is its index within its parent (calls removePart on its direct - * parent). - * - * @param \ZBateson\MailMimeParser\Message\MimePart $part - * @return int or null if not found - */ - public function removePart(MimePart $part) - { - $parent = $part->getParent(); - if ($this !== $parent && $parent !== null) { - return $parent->removePart($part); - } else { - $position = array_search($part, $this->parts, true); - if ($position !== false) { - array_splice($this->parts, $position, 1); - return $position; - } - } - return null; - } - - /** - * Removes all parts that are matched by the passed PartFilter. - * - * @param \ZBateson\MailMimeParser\Message\PartFilter $filter - */ - public function removeAllParts(PartFilter $filter = null) - { - foreach ($this->getAllParts($filter) as $part) { - $this->removePart($part); - } - } - - /** - * Returns the part at the given 0-based index, or null if none is set. - * - * Note that the first part returned is the current part itself. This is - * often desirable for queries with a PartFilter, e.g. looking for a - * MimePart with a specific Content-Type that may be satisfied by the - * current part. - * - * @param int $index - * @param PartFilter $filter - * @return \ZBateson\MailMimeParser\Message\MimePart - */ - public function getPart($index, PartFilter $filter = null) - { - $parts = $this->getAllParts($filter); - if (!isset($parts[$index])) { - return null; - } - return $parts[$index]; - } - - /** - * Returns the current part, all child parts, and child parts of all - * children optionally filtering them with the provided PartFilter. - * - * The first part returned is always the current MimePart. This is often - * desirable as it may be a valid MimePart for the provided PartFilter. - * - * @param PartFilter $filter an optional filter - * @return \ZBateson\MailMimeParser\Message\MimePart[] - */ - public function getAllParts(PartFilter $filter = null) - { - $aParts = [ $this ]; - foreach ($this->parts as $part) { - $aParts = array_merge($aParts, $part->getAllParts(null, true)); - } - if (!empty($filter)) { - return array_values(array_filter( - $aParts, - [ $filter, 'filter' ] - )); - } - return $aParts; - } - - /** - * Returns the total number of parts in this and all children. - * - * Note that the current part is considered, so the minimum getPartCount is - * 1 without a filter. - * - * @param PartFilter $filter - * @return int - */ - public function getPartCount(PartFilter $filter = null) - { - return count($this->getAllParts($filter)); - } - - /** - * Returns the direct child at the given 0-based index, or null if none is - * set. - * - * @param int $index - * @param PartFilter $filter - * @return \ZBateson\MailMimeParser\Message\MimePart - */ - public function getChild($index, PartFilter $filter = null) - { - $parts = $this->getChildParts($filter); - if (!isset($parts[$index])) { - return null; - } - return $parts[$index]; - } - - /** - * Returns all direct child parts. - * - * If a PartFilter is provided, the PartFilter is applied before returning. - * - * @param PartFilter $filter - * @return \ZBateson\MailMimeParser\Message\MimePart[] - */ - public function getChildParts(PartFilter $filter = null) - { - if ($filter !== null) { - return array_values(array_filter($this->parts, [ $filter, 'filter' ])); - } - return $this->parts; - } - - /** - * Returns the number of direct children under this part. - * - * @param PartFilter $filter - * @return int - */ - public function getChildCount(PartFilter $filter = null) - { - return count($this->getChildParts($filter)); - } - - /** - * Returns the part associated with the passed mime type if it exists. - * - * @param string $mimeType - * @return \ZBateson\MailMimeParser\Message\MimePart or null - */ - public function getPartByMimeType($mimeType, $index = 0) - { - return $this->getPart($index, PartFilter::fromContentType($mimeType)); - } - - /** - * Returns an array of all parts associated with the passed mime type if any - * exist or null otherwise. - * - * @param string $mimeType - * @return \ZBateson\MailMimeParser\Message\MimePart[] or null - */ - public function getAllPartsByMimeType($mimeType) - { - return $this->getAllParts(PartFilter::fromContentType($mimeType)); - } - - /** - * Returns the number of parts matching the passed $mimeType - * - * @param string $mimeType - * @return int - */ - public function getCountOfPartsByMimeType($mimeType) - { - return $this->getPartCount(PartFilter::fromContentType($mimeType)); - } - - /** - * Returns true if there's a content stream associated with the part. - * - * @return boolean - */ - public function hasContent() - { - if ($this->handle !== null) { - return true; - } - return false; - } - - /** - * Returns true if this part's mime type is multipart/* - * - * @return bool - */ - public function isMultiPart() - { - // casting to bool, preg_match returns 1 for true - return (bool) (preg_match( - '~multipart/\w+~i', - $this->getHeaderValue('Content-Type', 'text/plain') - )); - } - - /** - * Returns true if this part's mime type is text/plain, text/html or has a - * text/* and has a defined 'charset' attribute. - * - * @return bool - */ - public function isTextPart() - { - $type = $this->getHeaderValue('Content-Type', 'text/plain'); - if ($type === 'text/html' || $type === 'text/plain') { - return true; - } - $charset = $this->getHeaderParameter('Content-Type', 'charset'); - return ($charset !== null && preg_match( - '~text/\w+~i', - $this->getHeaderValue('Content-Type', 'text/plain') - )); - } - - /** - * Attaches the resource handle for the part's content. The attached handle - * is closed when the MimePart object is destroyed. - * - * @param resource $contentHandle - */ - public function attachContentResourceHandle($contentHandle) - { - if ($this->handle !== null && $this->handle !== $contentHandle) { - fclose($this->handle); - } - $this->handle = $contentHandle; - } - - /** - * Attaches the resource handle representing the original stream that - * created this part (including any sub-parts). The attached handle is - * closed when the MimePart object is destroyed. - * - * This stream is not modified or changed as the part is changed and is only - * set during parsing in MessageParser. - * - * @param resource $handle - */ - public function attachOriginalStreamHandle($handle) - { - if ($this->originalStreamHandle !== null && $this->originalStreamHandle !== $handle) { - fclose($this->originalStreamHandle); - } - $this->originalStreamHandle = $handle; - } - - /** - * Returns a resource stream handle allowing a user to read the original - * stream (including headers and child parts) that was used to create the - * current part. - * - * The part contains an original stream handle only if it was explicitly set - * by a call to MimePart::attachOriginalStreamHandle. MailMimeParser only - * sets this during the parsing phase in MessageParser, and is not otherwise - * changed or updated. New parts added below this part, changed headers, - * etc... would not be reflected in the returned stream handle. - * - * @return resource the resource handle or null if not set - */ - public function getOriginalStreamHandle() - { - if (is_resource($this->originalStreamHandle)) { - rewind($this->originalStreamHandle); - } - return $this->originalStreamHandle; - } - - /** - * Detaches the content resource handle from this part but does not close - * it. - */ - protected function detachContentResourceHandle() - { - $this->handle = null; - } - - /** - * Sets the content of the part to the passed string (effectively creates - * a php://temp stream with the passed content and calls - * attachContentResourceHandle with the opened stream). - * - * @param string $string - */ - public function setContent($string) - { - $handle = fopen('php://temp', 'r+'); - fwrite($handle, $string); - rewind($handle); - $this->attachContentResourceHandle($handle); - } - - /** - * Returns the resource stream handle for the part's content or null if not - * set. rewind() is called on the stream before returning it. - * - * The resource is automatically closed by MimePart's destructor and should - * not be closed otherwise. - * - * The returned resource handle is a stream with decoding filters appended - * to it. The attached filters are determined by looking at the part's - * Content-Encoding header. The following encodings are currently - * supported: - * - * - Quoted-Printable - * - Base64 - * - X-UUEncode - * - * UUEncode may be automatically attached for a message without a defined - * Content-Encoding and Content-Type if it has a UUEncoded part to support - * older non-mime message attachments. - * - * In addition, character encoding for text streams is converted to UTF-8 - * if {@link \ZBateson\MailMimeParser\Message\MimePart::isTextPart - * MimePart::isTextPart} returns true. - * - * @return resource - */ - public function getContentResourceHandle() - { - if (is_resource($this->handle)) { - rewind($this->handle); - } - return $this->handle; - } - - /** - * Shortcut to reading stream content and assigning it to a string. Returns - * null if the part doesn't have a content stream. - * - * @return string - */ - public function getContent() - { - if ($this->hasContent()) { - $text = stream_get_contents($this->handle); - rewind($this->handle); - return $text; - } - return null; - } - - /** - * Adds a header with the given $name and $value. - * - * Creates a new \ZBateson\MailMimeParser\Header\AbstractHeader object and - * registers it as a header. - * - * @param string $name - * @param string $value - */ - public function setRawHeader($name, $value) - { - $this->headers[strtolower($name)] = $this->headerFactory->newInstance($name, $value); - } - - /** - * Removes the header with the given name - * - * @param string $name - */ - public function removeHeader($name) - { - unset($this->headers[strtolower($name)]); - } - - /** - * Returns the AbstractHeader object for the header with the given $name - * - * Note that mime headers aren't case sensitive. - * - * @param string $name - * @return \ZBateson\MailMimeParser\Header\AbstractHeader - */ - public function getHeader($name) - { - if (isset($this->headers[strtolower($name)])) { - return $this->headers[strtolower($name)]; - } - return null; - } - - /** - * Returns the string value for the header with the given $name. - * - * Note that mime headers aren't case sensitive. - * - * @param string $name - * @param string $defaultValue - * @return string - */ - public function getHeaderValue($name, $defaultValue = null) - { - $header = $this->getHeader($name); - if ($header !== null) { - return $header->getValue(); - } - return $defaultValue; - } - - /** - * Returns the full array of headers for this part. - * - * @return \ZBateson\MailMimeParser\Header\AbstractHeader[] - */ - public function getHeaders() - { - return $this->headers; - } - - /** - * Returns a parameter of the header $header, given the parameter named - * $param. - * - * Only headers of type - * \ZBateson\MailMimeParser\Header\ParameterHeader have parameters. - * Content-Type and Content-Disposition are examples of headers with - * parameters. "Charset" is a common parameter of Content-Type. - * - * @param string $header - * @param string $param - * @param string $defaultValue - * @return string - */ - public function getHeaderParameter($header, $param, $defaultValue = null) - { - $obj = $this->getHeader($header); - if ($obj && $obj instanceof ParameterHeader) { - return $obj->getValueFor($param, $defaultValue); - } - return $defaultValue; - } - - /** - * Sets the parent part. - * - * @param \ZBateson\MailMimeParser\Message\MimePart $part - */ - public function setParent(MimePart $part) - { - $this->parent = $part; - } - - /** - * Returns this part's parent. - * - * @return \ZBateson\MailMimeParser\Message\MimePart - */ - public function getParent() - { - return $this->parent; - } -} diff --git a/src/Message/MimePartFactory.php b/src/Message/MimePartFactory.php deleted file mode 100644 index d8e747b3..00000000 --- a/src/Message/MimePartFactory.php +++ /dev/null @@ -1,73 +0,0 @@ -headerFactory = $headerFactory; - $this->messageWriterService = $messageWriterService; - } - - /** - * Constructs a new MimePart object and returns it - * - * @return \ZBateson\MailMimeParser\Message\MimePart - */ - public function newMimePart() - { - return new MimePart($this->headerFactory, $this->messageWriterService->getMimePartWriter()); - } - - /** - * Constructs a new NonMimePart object and returns it - * - * @return \ZBateson\MailMimeParser\Message\NonMimePart - */ - public function newNonMimePart() - { - return new NonMimePart($this->headerFactory, $this->messageWriterService->getMimePartWriter()); - } - - /** - * Constructs a new UUEncodedPart object and returns it - * - * @param int $mode - * @param string $filename - */ - public function newUUEncodedPart($mode = 0666, $filename = 'bin') - { - return new UUEncodedPart($this->headerFactory, $this->messageWriterService->getMimePartWriter(), $mode, $filename); - } -} diff --git a/src/Message/NonMimePart.php b/src/Message/NonMimePart.php deleted file mode 100644 index 8036a1dd..00000000 --- a/src/Message/NonMimePart.php +++ /dev/null @@ -1,35 +0,0 @@ -setRawHeader('Content-Type', 'text/plain'); - } -} diff --git a/src/Message/Part/Factory/MessagePartFactory.php b/src/Message/Part/Factory/MessagePartFactory.php new file mode 100644 index 00000000..6a5bd7e7 --- /dev/null +++ b/src/Message/Part/Factory/MessagePartFactory.php @@ -0,0 +1,123 @@ +streamFactory = $streamFactory; + $this->partStreamFilterManagerFactory = $psf; + } + + /** + * Sets a cached singleton instance. + * + * @param MessagePartFactory $instance + */ + protected static function setCachedInstance(MessagePartFactory $instance) + { + if (self::$instances === null) { + self::$instances = []; + } + $class = get_called_class(); + self::$instances[$class] = $instance; + } + + /** + * Returns a cached singleton instance if one exists, or null if one hasn't + * been created yet. + * + * @return MessagePartFactory + */ + protected static function getCachedInstance() + { + $class = get_called_class(); + if (self::$instances === null || !isset(self::$instances[$class])) { + return null; + } + return self::$instances[$class]; + } + + /** + * Returns the singleton instance for the class. + * + * @param StreamFactory $sdf + * @param PartStreamFilterManagerFactory $psf + * @param HeaderFactory $hf + * @param PartFilterFactory $pf + * @param MessageHelperService $mhs + * @return MessagePartFactory + */ + public static function getInstance( + StreamFactory $sdf, + PartStreamFilterManagerFactory $psf, + HeaderFactory $hf = null, + PartFilterFactory $pf = null, + MessageHelperService $mhs = null + ) { + $instance = static::getCachedInstance(); + if ($instance === null) { + $ref = new ReflectionClass(get_called_class()); + $n = $ref->getConstructor()->getNumberOfParameters(); + $args = []; + for ($i = 0; $i < $n; ++$i) { + $args[] = func_get_arg($i); + } + $instance = $ref->newInstanceArgs($args); + static::setCachedInstance($instance); + } + return $instance; + } + + /** + * Constructs a new MessagePart object and returns it + * + * @param PartBuilder $partBuilder + * @param StreamInterface $messageStream + * @return \ZBateson\MailMimeParser\Message\Part\MessagePart + */ + public abstract function newInstance(PartBuilder $partBuilder, StreamInterface $messageStream = null); +} diff --git a/src/Message/Part/Factory/MimePartFactory.php b/src/Message/Part/Factory/MimePartFactory.php new file mode 100644 index 00000000..64f880c6 --- /dev/null +++ b/src/Message/Part/Factory/MimePartFactory.php @@ -0,0 +1,77 @@ +headerFactory = $hf; + $this->partFilterFactory = $pf; + } + + /** + * Constructs a new MimePart object and returns it + * + * @param PartBuilder $partBuilder + * @param StreamInterface $messageStream + * @return \ZBateson\MailMimeParser\Message\Part\MimePart + */ + public function newInstance(PartBuilder $partBuilder, StreamInterface $messageStream = null) + { + $partStream = null; + $contentStream = null; + if ($messageStream !== null) { + $partStream = $this->streamFactory->getLimitedPartStream($messageStream, $partBuilder); + $contentStream = $this->streamFactory->getLimitedContentStream($messageStream, $partBuilder); + } + return new MimePart( + $this->partStreamFilterManagerFactory->newInstance(), + $this->streamFactory, + $this->partFilterFactory, + $this->headerFactory, + $partBuilder, + $partStream, + $contentStream + ); + } +} diff --git a/src/Message/Part/Factory/NonMimePartFactory.php b/src/Message/Part/Factory/NonMimePartFactory.php new file mode 100644 index 00000000..097119c9 --- /dev/null +++ b/src/Message/Part/Factory/NonMimePartFactory.php @@ -0,0 +1,42 @@ +streamFactory->getLimitedPartStream($messageStream, $partBuilder); + $contentStream = $this->streamFactory->getLimitedContentStream($messageStream, $partBuilder); + } + return new NonMimePart( + $this->partStreamFilterManagerFactory->newInstance(), + $this->streamFactory, + $partStream, + $contentStream + ); + } +} diff --git a/src/Message/Part/Factory/PartBuilderFactory.php b/src/Message/Part/Factory/PartBuilderFactory.php new file mode 100644 index 00000000..f4ffd864 --- /dev/null +++ b/src/Message/Part/Factory/PartBuilderFactory.php @@ -0,0 +1,53 @@ +headerFactory = $headerFactory; + } + + /** + * Constructs a new PartBuilder object and returns it + * + * @param \ZBateson\MailMimeParser\Message\Part\Factory\MessagePartFactory + * $messagePartFactory + * @return \ZBateson\MailMimeParser\Message\Part\PartBuilder + */ + public function newPartBuilder(MessagePartFactory $messagePartFactory) + { + return new PartBuilder( + $this->headerFactory, + $messagePartFactory + ); + } +} diff --git a/src/Message/Part/Factory/PartFactoryService.php b/src/Message/Part/Factory/PartFactoryService.php new file mode 100644 index 00000000..fab3e170 --- /dev/null +++ b/src/Message/Part/Factory/PartFactoryService.php @@ -0,0 +1,126 @@ +headerFactory = $headerFactory; + $this->partFilterFactory = $partFilterFactory; + $this->streamFactory = $streamFactory; + $this->partStreamFilterManagerFactory = $partStreamFilterManagerFactory; + $this->messageHelperService = $messageHelperService; + } + + /** + * Returns the MessageFactory singleton instance. + * + * @return MessageFactory + */ + public function getMessageFactory() + { + return MessageFactory::getInstance( + $this->streamFactory, + $this->partStreamFilterManagerFactory, + $this->headerFactory, + $this->partFilterFactory, + $this->messageHelperService + ); + } + + /** + * Returns the MimePartFactory singleton instance. + * + * @return MimePartFactory + */ + public function getMimePartFactory() + { + return MimePartFactory::getInstance( + $this->streamFactory, + $this->partStreamFilterManagerFactory, + $this->headerFactory, + $this->partFilterFactory + ); + } + + /** + * Returns the NonMimePartFactory singleton instance. + * + * @return NonMimePartFactory + */ + public function getNonMimePartFactory() + { + return NonMimePartFactory::getInstance( + $this->streamFactory, + $this->partStreamFilterManagerFactory + ); + } + + /** + * Returns the UUEncodedPartFactory singleton instance. + * + * @return UUEncodedPartFactory + */ + public function getUUEncodedPartFactory() + { + return UUEncodedPartFactory::getInstance( + $this->streamFactory, + $this->partStreamFilterManagerFactory + ); + } +} diff --git a/src/Message/Part/Factory/PartStreamFilterManagerFactory.php b/src/Message/Part/Factory/PartStreamFilterManagerFactory.php new file mode 100644 index 00000000..6d3ec374 --- /dev/null +++ b/src/Message/Part/Factory/PartStreamFilterManagerFactory.php @@ -0,0 +1,43 @@ +streamFactory = $streamFactory; + } + + /** + * Constructs a new PartStreamFilterManager object and returns it. + * + * @return \ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager + */ + public function newInstance() + { + return new PartStreamFilterManager($this->streamFactory); + } +} diff --git a/src/Message/Part/Factory/UUEncodedPartFactory.php b/src/Message/Part/Factory/UUEncodedPartFactory.php new file mode 100644 index 00000000..43b3e07e --- /dev/null +++ b/src/Message/Part/Factory/UUEncodedPartFactory.php @@ -0,0 +1,43 @@ +streamFactory->getLimitedPartStream($messageStream, $partBuilder); + $contentStream = $this->streamFactory->getLimitedContentStream($messageStream, $partBuilder); + } + return new UUEncodedPart( + $this->partStreamFilterManagerFactory->newInstance(), + $this->streamFactory, + $partBuilder, + $partStream, + $contentStream + ); + } +} diff --git a/src/Message/Part/MessagePart.php b/src/Message/Part/MessagePart.php new file mode 100644 index 00000000..420182a4 --- /dev/null +++ b/src/Message/Part/MessagePart.php @@ -0,0 +1,407 @@ +partStreamFilterManager = $partStreamFilterManager; + $this->streamFactory = $streamFactory; + + $this->stream = $stream; + $this->contentStream = $contentStream; + if ($contentStream !== null) { + $partStreamFilterManager->setStream( + $contentStream + ); + } + } + + /** + * Overridden to close streams. + */ + public function __destruct() + { + if ($this->stream !== null) { + $this->stream->close(); + } + if ($this->contentStream !== null) { + $this->contentStream->close(); + } + } + + /** + * Called when operations change the content of the MessagePart. + * + * The function causes calls to getStream() to return a dynamic + * MessagePartStream instead of the read stream for this MessagePart and all + * parent MessageParts. + */ + protected function onChange() + { + $this->markAsChanged(); + if ($this->parent !== null) { + $this->parent->onChange(); + } + } + + /** + * Marks the part as changed, forcing the part to be rewritten when saved. + * + * Normal operations to a MessagePart automatically mark the part as + * changed and markAsChanged() doesn't need to be called in those cases. + * + * The function can be called to indicate an external change that requires + * rewriting this part, for instance changing a message from a non-mime + * message to a mime one, would require rewriting non-mime children to + * insure suitable headers are written. + * + * Internally, the function discards the part's stream, forcing a stream to + * be created when calling getStream(). + */ + public function markAsChanged() + { + // the stream is not closed because $this->contentStream may still be + // attached to it. GuzzleHttp will clean it up when destroyed. + $this->stream = null; + } + + /** + * Returns true if there's a content stream associated with the part. + * + * @return boolean + */ + public function hasContent() + { + return ($this->contentStream !== null); + } + + /** + * Returns true if this part's mime type is text/plain, text/html or has a + * text/* and has a defined 'charset' attribute. + * + * @return bool + */ + public abstract function isTextPart(); + + /** + * Returns the mime type of the content. + * + * @return string + */ + public abstract function getContentType(); + + /** + * Returns the charset of the content, or null if not applicable/defined. + * + * @return string + */ + public abstract function getCharset(); + + /** + * Returns the content's disposition. + * + * @return string + */ + public abstract function getContentDisposition(); + + /** + * Returns the content-transfer-encoding used for this part. + * + * @return string + */ + public abstract function getContentTransferEncoding(); + + /** + * Returns a filename for the part if one is defined, or null otherwise. + * + * @return string + */ + public function getFilename() + { + return null; + } + + /** + * Returns true if the current part is a mime part. + * + * @return bool + */ + public abstract function isMime(); + + /** + * Rewrite me + * + * @return resource the resource handle + */ + public function getHandle() + { + return StreamWrapper::getResource($this->getStream()); + } + + /** + * Write me + * + * @return StreamInterface the resource handle + */ + public function getStream() + { + if ($this->stream === null) { + return $this->streamFactory->newMessagePartStream($this); + } + $this->stream->rewind(); + return $this->stream; + } + + /** + * Overrides the default character set used for reading content from content + * streams in cases where a user knows the source charset is not what is + * specified. + * + * If set, the returned value from MessagePart::getCharset is ignored. + * + * Note that setting an override on a Message and calling getTextStream, + * getTextContent, getHtmlStream or getHtmlContent will not be applied to + * those sub-parts, unless the text/html part is the Message itself. + * Instead, Message:getTextPart() should be called, and setCharsetOverride + * called on the returned MessagePart. + * + * @param string $charsetOverride + * @param boolean $onlyIfNoCharset if true, $charsetOverride is used only if + * getCharset returns null. + */ + public function setCharsetOverride($charsetOverride, $onlyIfNoCharset = false) + { + if (!$onlyIfNoCharset || $this->getCharset() === null) { + $this->charsetOverride = $charsetOverride; + } + } + + /** + * Returns a new resource stream handle for the part's content or null if + * the part doesn't have a content section. + * + * The returned resource handle is a resource stream with decoding filters + * appended to it. The attached filters are determined by looking at the + * part's Content-Transfer-Encoding and Content-Type headers unless a + * charset override is set. The following transfer encodings are supported: + * + * - quoted-printable + * - base64 + * - x-uuencode + * + * In addition, the charset of the underlying stream is converted to the + * passed $charset if the content is known to be text. + * + * @param string $charset + * @return resource + */ + public function getContentResourceHandle($charset = MailMimeParser::DEFAULT_CHARSET) + { + $stream = $this->getContentStream($charset); + if ($stream !== null) { + return StreamWrapper::getResource($stream); + } + return null; + } + + /** + * Returns the StreamInterface for the part's content or null if the part + * doesn't have a content section. + * + * Because the returned stream may be a shared object if called multiple + * times, the function isn't exposed publicly. If called multiple times + * with the same $charset, and the value of the part's + * Content-Transfer-Encoding header not having changed, the returned stream + * is the same instance and may need to be rewound. + * + * Note that PartStreamFilterManager rewinds the stream before returning it. + * + * @param string $charset + * @return StreamInterface + */ + public function getContentStream($charset = MailMimeParser::DEFAULT_CHARSET) + { + if ($this->hasContent()) { + $tr = ($this->ignoreTransferEncoding) ? '' : $this->getContentTransferEncoding(); + $ch = ($this->charsetOverride !== null) ? $this->charsetOverride : $this->getCharset(); + return $this->partStreamFilterManager->getContentStream( + $tr, + $ch, + $charset + ); + } + return null; + } + + /** + * Shortcut to reading stream content and assigning it to a string. Returns + * null if the part doesn't have a content stream. + * + * The returned string is encoded to the passed $charset character encoding, + * defaulting to UTF-8. + * + * @return string + */ + public function getContent($charset = MailMimeParser::DEFAULT_CHARSET) + { + $stream = $this->getContentStream($charset); + if ($stream !== null) { + return $stream->getContents(); + } + return null; + } + + /** + * Returns this part's parent. + * + * @return \ZBateson\MailMimeParser\Message\Part\MimePart + */ + public function getParent() + { + return $this->parent; + } + + /** + * Attaches the stream or resource handle for the part's content. The + * stream is closed when another stream is attached, or the MimePart is + * destroyed. + * + * @param StreamInterface $stream + * @param string $streamCharset + */ + public function attachContentStream(StreamInterface $stream, $streamCharset = MailMimeParser::DEFAULT_CHARSET) + { + if ($this->contentStream !== null && $this->contentStream !== $stream) { + $this->contentStream->close(); + } + $this->contentStream = $stream; + $ch = ($this->charsetOverride !== null) ? $this->charsetOverride : $this->getCharset(); + if ($ch !== null && $streamCharset !== $ch) { + $this->charsetOverride = $streamCharset; + } + $this->ignoreTransferEncoding = true; + $this->partStreamFilterManager->setStream($stream); + $this->onChange(); + } + + /** + * Detaches and closes the content stream. + */ + public function detachContentStream() + { + $this->contentStream = null; + $this->partStreamFilterManager->setStream(null); + $this->onChange(); + } + + /** + * Sets the content of the part to the passed string. + * + * @param string|resource $stringOrHandle + * @param string $charset + */ + public function setContent($stringOrHandle, $charset = MailMimeParser::DEFAULT_CHARSET) + { + $stream = Psr7\stream_for($stringOrHandle); + $this->attachContentStream($stream, $charset); + // this->onChange called in attachContentStream + } + + /** + * Saves the message/part as to the passed resource handle. + * + * @param resource|StreamInterface $streamOrHandle + */ + public function save($streamOrHandle) + { + $message = $this->getStream(); + $message->rewind(); + if (!($streamOrHandle instanceof StreamInterface)) { + $streamOrHandle = Psr7\stream_for($streamOrHandle); + } + Psr7\copy_to_stream($message, $streamOrHandle); + // don't close when out of scope + $streamOrHandle->detach(); + } + + /** + * Returns the message/part as a string. + * + * Convenience method for calling getStream()->getContents(). + * + * @return string + */ + public function __toString() + { + return $this->getStream()->getContents(); + } +} diff --git a/src/Message/Part/MimePart.php b/src/Message/Part/MimePart.php new file mode 100644 index 00000000..1aef7f87 --- /dev/null +++ b/src/Message/Part/MimePart.php @@ -0,0 +1,136 @@ +getContentType() + )); + } + + /** + * Returns a filename for the part if one is defined, or null otherwise. + * + * @return string + */ + public function getFilename() + { + return $this->getHeaderParameter( + 'Content-Disposition', + 'filename', + $this->getHeaderParameter( + 'Content-Type', + 'name' + ) + ); + } + + /** + * Returns true. + * + * @return bool + */ + public function isMime() + { + return true; + } + + /** + * Returns true if this part's mime type is text/plain, text/html or if the + * Content-Type header defines a charset. + * + * @return bool + */ + public function isTextPart() + { + return ($this->getCharset() !== null); + } + + /** + * Returns the lower-cased, trimmed value of the Content-Type header. + * + * Parses the Content-Type header, defaults to returning text/plain if not + * defined. + * + * @return string + */ + public function getContentType($default = 'text/plain') + { + return trim(strtolower($this->getHeaderValue('Content-Type', $default))); + } + + /** + * Returns the upper-cased charset of the Content-Type header's charset + * parameter if set, ISO-8859-1 if the Content-Type is text/plain or + * text/html and the charset parameter isn't set, or null otherwise. + * + * @return string + */ + public function getCharset() + { + $charset = $this->getHeaderParameter('Content-Type', 'charset'); + if ($charset === null) { + $contentType = $this->getContentType(); + if ($contentType === 'text/plain' || $contentType === 'text/html') { + return 'ISO-8859-1'; + } + return null; + } + return trim(strtoupper($charset)); + } + + /** + * Returns the content's disposition, defaulting to 'inline' if not set. + * + * @return string + */ + public function getContentDisposition($default = 'inline') + { + return strtolower($this->getHeaderValue('Content-Disposition', $default)); + } + + /** + * Returns the content-transfer-encoding used for this part, defaulting to + * '7bit' if not set. + * + * @return string + */ + public function getContentTransferEncoding($default = '7bit') + { + static $translated = [ + 'x-uue' => 'x-uuencode', + 'uue' => 'x-uuencode', + 'uuencode' => 'x-uuencode' + ]; + $type = strtolower($this->getHeaderValue('Content-Transfer-Encoding', $default)); + if (isset($translated[$type])) { + return $translated[$type]; + } + return $type; + } +} diff --git a/src/Message/Part/NonMimePart.php b/src/Message/Part/NonMimePart.php new file mode 100644 index 00000000..4239f06f --- /dev/null +++ b/src/Message/Part/NonMimePart.php @@ -0,0 +1,80 @@ +headerFactory = $headerFactory; + $this->headers['contenttype'] = $partBuilder->getContentType(); + $this->rawHeaders = $partBuilder->getRawHeaders(); + } + + /** + * Returns the string in lower-case, and with non-alphanumeric characters + * stripped out. + * + * @param string $header + * @return string + */ + private function getNormalizedHeaderName($header) + { + return preg_replace('/[^a-z0-9]/', '', strtolower($header)); + } + + /** + * Returns the AbstractHeader object for the header with the given $name + * + * Note that mime headers aren't case sensitive. + * + * @param string $name + * @return AbstractHeader + */ + public function getHeader($name) + { + $nameKey = $this->getNormalizedHeaderName($name); + if (isset($this->rawHeaders[$nameKey])) { + if (!isset($this->headers[$nameKey])) { + $this->headers[$nameKey] = $this->headerFactory->newInstance( + $this->rawHeaders[$nameKey][0], + $this->rawHeaders[$nameKey][1] + ); + } + return $this->headers[$nameKey]; + } + return null; + } + + /** + * Returns an array of all headers for the mime part with the first element + * holding the name, and the second its value. + * + * @return string[][] + */ + public function getRawHeaders() + { + return array_values($this->rawHeaders); + } + + /** + * Returns the string value for the header with the given $name. + * + * Note that mime headers aren't case sensitive. + * + * @param string $name + * @param string $defaultValue + * @return string + */ + public function getHeaderValue($name, $defaultValue = null) + { + $header = $this->getHeader($name); + if ($header !== null) { + return $header->getValue(); + } + return $defaultValue; + } + + /** + * Returns a parameter of the header $header, given the parameter named + * $param. + * + * Only headers of type + * \ZBateson\MailMimeParser\Header\ParameterHeader have parameters. + * Content-Type and Content-Disposition are examples of headers with + * parameters. "Charset" is a common parameter of Content-Type. + * + * @param string $header + * @param string $param + * @param string $defaultValue + * @return string + */ + public function getHeaderParameter($header, $param, $defaultValue = null) + { + $obj = $this->getHeader($header); + if ($obj && $obj instanceof ParameterHeader) { + return $obj->getValueFor($param, $defaultValue); + } + return $defaultValue; + } + + /** + * Adds a header with the given $name and $value. + * + * Creates a new \ZBateson\MailMimeParser\Header\AbstractHeader object and + * registers it as a header. + * + * @param string $name + * @param string $value + */ + public function setRawHeader($name, $value) + { + $normalized = $this->getNormalizedHeaderName($name); + $header = $this->headerFactory->newInstance($name, $value); + $this->headers[$normalized] = $header; + $this->rawHeaders[$normalized] = [ + $header->getName(), + $header->getRawValue() + ]; + $this->onChange(); + } + + /** + * Removes the header with the given name + * + * @param string $name + */ + public function removeHeader($name) + { + $normalized = $this->getNormalizedHeaderName($name); + unset($this->headers[$normalized], $this->rawHeaders[$normalized]); + $this->onChange(); + } +} diff --git a/src/Message/Part/ParentPart.php b/src/Message/Part/ParentPart.php new file mode 100644 index 00000000..08d8881a --- /dev/null +++ b/src/Message/Part/ParentPart.php @@ -0,0 +1,281 @@ +partFilterFactory = $partFilterFactory; + + $pbChildren = $partBuilder->getChildren(); + if (!empty($pbChildren)) { + $this->children = array_map(function ($child) use ($stream) { + $childPart = $child->createMessagePart($stream); + $childPart->parent = $this; + return $childPart; + }, $pbChildren); + } + } + + /** + * Returns all parts, including the current object, and all children below + * it (including children of children, etc...) + * + * @return MessagePart[] + */ + protected function getAllNonFilteredParts() + { + $parts = [ $this ]; + foreach ($this->children as $part) { + if ($part instanceof MimePart) { + $parts = array_merge( + $parts, + $part->getAllNonFilteredParts() + ); + } else { + array_push($parts, $part); + } + } + return $parts; + } + + /** + * Returns the part at the given 0-based index, or null if none is set. + * + * Note that the first part returned is the current part itself. This is + * often desirable for queries with a PartFilter, e.g. looking for a + * MessagePart with a specific Content-Type that may be satisfied by the + * current part. + * + * @param int $index + * @param PartFilter $filter + * @return MessagePart + */ + public function getPart($index, PartFilter $filter = null) + { + $parts = $this->getAllParts($filter); + if (!isset($parts[$index])) { + return null; + } + return $parts[$index]; + } + + /** + * Returns the current part, all child parts, and child parts of all + * children optionally filtering them with the provided PartFilter. + * + * The first part returned is always the current MimePart. This is often + * desirable as it may be a valid MimePart for the provided PartFilter. + * + * @param PartFilter $filter an optional filter + * @return MessagePart[] + */ + public function getAllParts(PartFilter $filter = null) + { + $parts = $this->getAllNonFilteredParts(); + if (!empty($filter)) { + return array_values(array_filter( + $parts, + [ $filter, 'filter' ] + )); + } + return $parts; + } + + /** + * Returns the total number of parts in this and all children. + * + * Note that the current part is considered, so the minimum getPartCount is + * 1 without a filter. + * + * @param PartFilter $filter + * @return int + */ + public function getPartCount(PartFilter $filter = null) + { + return count($this->getAllParts($filter)); + } + + /** + * Returns the direct child at the given 0-based index, or null if none is + * set. + * + * @param int $index + * @param PartFilter $filter + * @return MessagePart + */ + public function getChild($index, PartFilter $filter = null) + { + $parts = $this->getChildParts($filter); + if (!isset($parts[$index])) { + return null; + } + return $parts[$index]; + } + + /** + * Returns all direct child parts. + * + * If a PartFilter is provided, the PartFilter is applied before returning. + * + * @param PartFilter $filter + * @return MessagePart[] + */ + public function getChildParts(PartFilter $filter = null) + { + if ($filter !== null) { + return array_values(array_filter($this->children, [ $filter, 'filter' ])); + } + return $this->children; + } + + /** + * Returns the number of direct children under this part. + * + * @param PartFilter $filter + * @return int + */ + public function getChildCount(PartFilter $filter = null) + { + return count($this->getChildParts($filter)); + } + + /** + * Returns the part associated with the passed mime type if it exists. + * + * @param string $mimeType + * @return MessagePart|null + */ + public function getPartByMimeType($mimeType, $index = 0) + { + $partFilter = $this->partFilterFactory->newFilterFromContentType($mimeType); + return $this->getPart($index, $partFilter); + } + + /** + * Returns an array of all parts associated with the passed mime type if any + * exist or null otherwise. + * + * @param string $mimeType + * @return MessagePart[] or null + */ + public function getAllPartsByMimeType($mimeType) + { + $partFilter = $this->partFilterFactory->newFilterFromContentType($mimeType); + return $this->getAllParts($partFilter); + } + + /** + * Returns the number of parts matching the passed $mimeType + * + * @param string $mimeType + * @return int + */ + public function getCountOfPartsByMimeType($mimeType) + { + $partFilter = $this->partFilterFactory->newFilterFromContentType($mimeType); + return $this->getPartCount($partFilter); + } + + /** + * Registers the passed part as a child of the current part. + * + * If the $position parameter is non-null, adds the part at the passed + * position index. + * + * @param MessagePart $part + * @param int $position + */ + public function addChild(MessagePart $part, $position = null) + { + if ($part !== $this) { + $part->parent = $this; + array_splice( + $this->children, + ($position === null) ? count($this->children) : $position, + 0, + [ $part ] + ); + $this->onChange(); + } + } + + /** + * Removes the child part from this part and returns its position or + * null if it wasn't found. + * + * Note that if the part is not a direct child of this part, the returned + * position is its index within its parent (calls removePart on its direct + * parent). + * + * @param MessagePart $part + * @return int or null if not found + */ + public function removePart(MessagePart $part) + { + $parent = $part->getParent(); + if ($this !== $parent && $parent !== null) { + return $parent->removePart($part); + } else { + $position = array_search($part, $this->children, true); + if ($position !== false) { + array_splice($this->children, $position, 1); + $this->onChange(); + return $position; + } + } + return null; + } + + /** + * Removes all parts that are matched by the passed PartFilter. + * + * @param \ZBateson\MailMimeParser\Message\PartFilter $filter + */ + public function removeAllParts(PartFilter $filter = null) + { + foreach ($this->getAllParts($filter) as $part) { + $this->removePart($part); + } + } +} diff --git a/src/Message/Part/PartBuilder.php b/src/Message/Part/PartBuilder.php new file mode 100644 index 00000000..71addc88 --- /dev/null +++ b/src/Message/Part/PartBuilder.php @@ -0,0 +1,433 @@ + value pairs of properties passed on to the + * $messagePartFactory when constructing the Message and its children. + */ + private $properties = []; + + /** + * @var ZBateson\MailMimeParser\Header\ParameterHeader parsed content-type + * header. + */ + private $contentType = null; + + /** + * Sets up class dependencies. + * + * @param HeaderFactory $hf + * @param MessagePartFactory $mpf + */ + public function __construct( + HeaderFactory $hf, + MessagePartFactory $mpf + ) { + $this->headerFactory = $hf; + $this->messagePartFactory = $mpf; + } + + /** + * Adds a header with the given $name and $value to the headers array. + * + * Removes non-alphanumeric characters from $name, and sets it to lower-case + * to use as a key in the private headers array. Sets the original $name + * and $value as elements in the headers' array value for the calculated + * key. + * + * @param string $name + * @param string $value + */ + public function addHeader($name, $value) + { + $nameKey = preg_replace('/[^a-z0-9]/', '', strtolower($name)); + $this->headers[$nameKey] = [$name, $value]; + } + + /** + * Returns the raw headers added to this PartBuilder as an array consisting + * of: + * + * Keys set to the name of the header, in all lowercase, and with non- + * alphanumeric characters removed (e.g. Content-Type becomes contenttype). + * + * The value is an array of two elements. The first is the original header + * name (e.g. Content-Type) and the second is the raw string value of the + * header, e.g. 'text/html; charset=utf8'. + * + * @return array + */ + public function getRawHeaders() + { + return $this->headers; + } + + /** + * Sets the specified property denoted by $name to $value. + * + * @param string $name + * @param mixed $value + */ + public function setProperty($name, $value) + { + $this->properties[$name] = $value; + } + + /** + * Returns the value of the property with the given $name. + * + * @param string $name + * @return mixed + */ + public function getProperty($name) + { + if (!isset($this->properties[$name])) { + return null; + } + return $this->properties[$name]; + } + + /** + * Registers the passed PartBuilder as a child of the current PartBuilder. + * + * @param \ZBateson\MailMimeParser\Message\PartBuilder $partBuilder + */ + public function addChild(PartBuilder $partBuilder) + { + $partBuilder->parent = $this; + // discard parts added after the end boundary + if (!$this->endBoundaryFound) { + $this->children[] = $partBuilder; + } + } + + /** + * Returns all children PartBuilder objects. + * + * @return \ZBateson\MailMimeParser\Message\PartBuilder[] + */ + public function getChildren() + { + return $this->children; + } + + /** + * Returns this PartBuilder's parent. + * + * @return PartBuilder + */ + public function getParent() + { + return $this->parent; + } + + /** + * Returns true if either a Content-Type or Mime-Version header are defined + * in this PartBuilder's headers. + * + * @return boolean + */ + public function isMime() + { + return (isset($this->headers['contenttype']) + || isset($this->headers['mimeversion'])); + } + + /** + * Returns a ParameterHeader representing the parsed Content-Type header for + * this PartBuilder. + * + * @return \ZBateson\MailMimeParser\Header\ParameterHeader + */ + public function getContentType() + { + if ($this->contentType === null && isset($this->headers['contenttype'])) { + $this->contentType = $this->headerFactory->newInstance( + 'Content-Type', + $this->headers['contenttype'][1] + ); + } + return $this->contentType; + } + + /** + * Returns the parsed boundary parameter of the Content-Type header if set + * for a multipart message part. + * + * @return string + */ + public function getMimeBoundary() + { + if ($this->mimeBoundary === false) { + $this->mimeBoundary = null; + $contentType = $this->getContentType(); + if ($contentType !== null) { + $this->mimeBoundary = $contentType->getValueFor('boundary'); + } + } + return $this->mimeBoundary; + } + + /** + * Returns true if this part's content-type is multipart/* + * + * @return boolean + */ + public function isMultiPart() + { + $contentType = $this->getContentType(); + if ($contentType !== null) { + // casting to bool, preg_match returns 1 for true + return (bool) (preg_match( + '~multipart/\w+~i', + $contentType->getValue() + )); + } + return false; + } + + /** + * Returns true if the passed $line of read input matches this PartBuilder's + * mime boundary, or any of its parent's mime boundaries for a multipart + * message. + * + * If the passed $line is the ending boundary for the current PartBuilder, + * $this->isEndBoundaryFound will return true after. + * + * @param string $line + * @return boolean + */ + public function setEndBoundaryFound($line) + { + $boundary = $this->getMimeBoundary(); + if ($this->parent !== null && $this->parent->setEndBoundaryFound($line)) { + $this->parentBoundaryFound = true; + return true; + } elseif ($boundary !== null) { + if ($line === "--$boundary--") { + $this->endBoundaryFound = true; + return true; + } elseif ($line === "--$boundary") { + return true; + } + } + return false; + } + + /** + * Returns true if MessageParser passed an input line to setEndBoundary that + * matches a parent's mime boundary, and the following input belongs to a + * new part under its parent. + * + * @return boolean + */ + public function isParentBoundaryFound() + { + return ($this->parentBoundaryFound); + } + + /** + * Called once EOF is reached while reading content. The method sets the + * flag used by PartBuilder::isParentBoundaryFound to true on this part and + * all parent PartBuilders. + */ + public function setEof() + { + $this->parentBoundaryFound = true; + if ($this->parent !== null) { + $this->parent->parentBoundaryFound = true; + } + } + + /** + * Returns false if this part has a parent part in which endBoundaryFound is + * set to true (i.e. this isn't a discardable part following the parent's + * end boundary line). + * + * @return booelan + */ + public function canHaveHeaders() + { + return ($this->parent === null || !$this->parent->endBoundaryFound); + } + + public function getStreamPartStartOffset() + { + if ($this->parent) { + return $this->streamPartStartPos - $this->parent->streamPartStartPos; + } + return $this->streamPartStartPos; + } + + public function getStreamPartLength() + { + return $this->streamPartEndPos - $this->streamPartStartPos; + } + + public function getStreamContentStartOffset() + { + if ($this->parent) { + return $this->streamContentStartPos - $this->parent->streamPartStartPos; + } + return $this->streamContentStartPos; + } + + public function getStreamContentLength() + { + return $this->streamContentEndPos - $this->streamContentStartPos; + } + + /** + * Sets the start position of the part in the input stream. + * + * @param int $streamPartStartPos + */ + public function setStreamPartStartPos($streamPartStartPos) + { + $this->streamPartStartPos = $streamPartStartPos; + } + + /** + * Sets the end position of the part in the input stream, and also calls + * parent->setParentStreamPartEndPos to expand to parent parts. + * + * @param int $streamPartEndPos + */ + public function setStreamPartEndPos($streamPartEndPos) + { + $this->streamPartEndPos = $streamPartEndPos; + if ($this->parent !== null) { + $this->parent->setStreamPartEndPos($streamPartEndPos); + } + } + + /** + * Sets the start position of the content in the input stream. + * + * @param int $streamContentStartPos + */ + public function setStreamContentStartPos($streamContentStartPos) + { + $this->streamContentStartPos = $streamContentStartPos; + } + + /** + * Sets the end position of the content and part in the input stream. + * + * @param int $streamContentEndPos + */ + public function setStreamPartAndContentEndPos($streamContentEndPos) + { + $this->streamContentEndPos = $streamContentEndPos; + $this->setStreamPartEndPos($streamContentEndPos); + } + + /** + * Creates a MessagePart and returns it using the PartBuilder's + * MessagePartFactory passed in during construction. + * + * @param StreamInterface $stream + * @return MessagePart + */ + public function createMessagePart(StreamInterface $stream = null) + { + return $this->messagePartFactory->newInstance( + $this, + $stream + ); + } +} diff --git a/src/Message/Part/PartStreamFilterManager.php b/src/Message/Part/PartStreamFilterManager.php new file mode 100644 index 00000000..2ce8191d --- /dev/null +++ b/src/Message/Part/PartStreamFilterManager.php @@ -0,0 +1,221 @@ + null, + 'filter' => null + ]; + + /** + * @var array map of the active charset filter on the current handle. + */ + private $charset = [ + 'from' => null, + 'to' => null, + 'filter' => null + ]; + + /** + * @var StreamFactory used to apply psr7 stream decorators to the + * attached StreamInterface based on encoding. + */ + private $streamFactory; + + /** + * @var string name of stream filter handling character set conversion + */ + private $charsetConversionFilter; + + /** + * Sets up filter names used for stream_filter_append + * + * @param StreamFactory $streamFactory + */ + public function __construct(StreamFactory $streamFactory) + { + $this->streamFactory = $streamFactory; + $this->charsetConversionFilter = ''; + } + + /** + * Sets the URL used to open the content resource handle. + * + * The function also closes the currently attached handle if any. + * + * @param StreamInterface $stream + */ + public function setStream(StreamInterface $stream = null) + { + $this->stream = $stream; + $this->filteredStream = null; + } + + /** + * Returns true if the attached stream filter used for decoding the content + * on the current handle is different from the one passed as an argument. + * + * @param string $transferEncoding + * @return boolean + */ + private function isTransferEncodingFilterChanged($transferEncoding) + { + return ($transferEncoding !== $this->encoding['type']); + } + + /** + * Returns true if the attached stream filter used for charset conversion on + * the current handle is different from the one needed based on the passed + * arguments. + * + * @param string $fromCharset + * @param string $toCharset + * @return boolean + */ + private function isCharsetFilterChanged($fromCharset, $toCharset) + { + return ($fromCharset !== $this->charset['from'] + || $toCharset !== $this->charset['to']); + } + + /** + * Attaches a decoding filter to the attached content handle, for the passed + * $transferEncoding. + * + * @param string $transferEncoding + */ + protected function attachTransferEncodingFilter($transferEncoding) + { + if ($this->filteredStream !== null) { + $this->encoding['type'] = $transferEncoding; + $assign = null; + switch ($transferEncoding) { + case 'base64': + $assign = $this->streamFactory->newBase64Stream($this->filteredStream); + break; + case 'x-uuencode': + $assign = $this->streamFactory->newUUStream($this->filteredStream); + break; + case 'quoted-printable': + $assign = $this->streamFactory->newQuotedPrintableStream($this->filteredStream); + break; + } + if ($assign !== null) { + $this->filteredStream = new CachingStream($assign); + } + } + } + + /** + * Attaches a charset conversion filter to the attached content handle, for + * the passed arguments. + * + * @param string $fromCharset the character set the content is encoded in + * @param string $toCharset the target encoding to return + */ + protected function attachCharsetFilter($fromCharset, $toCharset) + { + if ($this->filteredStream !== null) { + if (!empty($fromCharset) && !empty($toCharset)) { + $this->filteredStream = new CachingStream($this->streamFactory->newCharsetStream( + $this->filteredStream, + $fromCharset, + $toCharset + )); + } + $this->charset['from'] = $fromCharset; + $this->charset['to'] = $toCharset; + } + } + + /** + * Closes the attached resource handle, resets mapped encoding and charset + * filters, and reopens the handle seeking back to the current position. + * + * Note that closing/reopening is done because of the following differences + * discovered between hhvm (up to 3.18 at least) and php: + * + * o stream_filter_remove wasn't triggering php_user_filter's onClose + * callback + * o read operations performed after stream_filter_remove weren't calling + * filter on php_user_filter + * + * It seems stream_filter_remove doesn't work on hhvm, or isn't implemented + * in the same way -- so closing and reopening seems to solve that. + */ + public function reset() + { + $this->encoding = [ + 'type' => null, + 'filter' => null + ]; + $this->charset = [ + 'from' => null, + 'to' => null, + 'filter' => null + ]; + $this->stream->rewind(); + $this->filteredStream = $this->stream; + } + + /** + * Checks what transfer-encoding decoder filters and charset conversion + * filters are attached on the handle, closing/reopening the handle if + * different, before attaching relevant filters for the passed + * $transferEncoding and charset arguments, and returning a StreamInterface. + * + * @param string $transferEncoding + * @param string $fromCharset the character set the content is encoded in + * @param string $toCharset the target encoding to return + * @return StreamInterface + */ + public function getContentStream($transferEncoding, $fromCharset, $toCharset) + { + if ($this->stream === null) { + return null; + } + if ($this->filteredStream === null + || $this->isTransferEncodingFilterChanged($transferEncoding) + || $this->isCharsetFilterChanged($fromCharset, $toCharset)) { + $this->reset(); + $this->attachTransferEncodingFilter($transferEncoding); + $this->attachCharsetFilter($fromCharset, $toCharset); + } + $this->filteredStream->rewind(); + return $this->filteredStream; + } +} diff --git a/src/Message/Part/UUEncodedPart.php b/src/Message/Part/UUEncodedPart.php new file mode 100644 index 00000000..220f5d9c --- /dev/null +++ b/src/Message/Part/UUEncodedPart.php @@ -0,0 +1,149 @@ +mode = $partBuilder->getProperty('mode'); + $this->filename = $partBuilder->getProperty('filename'); + } + + /** + * Returns the file mode included in the uuencoded header for this part. + * + * @return int + */ + public function getUnixFileMode() + { + return $this->mode; + } + + /** + * Returns the filename included in the uuencoded header for this part. + * + * @return string + */ + public function getFilename() + { + return $this->filename; + } + + /** + * Sets the unix file mode for the uuencoded header. + * + * @param int $mode + */ + public function setUnixFileMode($mode) + { + $this->mode = $mode; + $this->onChange(); + } + + /** + * Sets the filename included in the uuencoded header. + * + * @param string $filename + */ + public function setFilename($filename) + { + $this->filename = $filename; + $this->onChange(); + } + + /** + * Returns false. + * + * @return bool + */ + public function isTextPart() + { + return false; + } + + /** + * Returns text/plain + * + * @return string + */ + public function getContentType() + { + return 'application/octet-stream'; + } + + /** + * Returns null + * + * @return string + */ + public function getCharset() + { + return null; + } + + /** + * Returns 'inline'. + * + * @return string + */ + public function getContentDisposition() + { + return 'attachment'; + } + + /** + * Returns 'x-uuencode'. + * + * @return string + */ + public function getContentTransferEncoding() + { + return 'x-uuencode'; + } +} diff --git a/src/Message/PartFilter.php b/src/Message/PartFilter.php index 47f6fe35..94805e76 100644 --- a/src/Message/PartFilter.php +++ b/src/Message/PartFilter.php @@ -6,6 +6,8 @@ */ namespace ZBateson\MailMimeParser\Message; +use ZBateson\MailMimeParser\Message\Part\MessagePart; +use ZBateson\MailMimeParser\Message\Part\MimePart; use InvalidArgumentException; /** @@ -55,14 +57,19 @@ class PartFilter * @var int an included filter must be included in a part */ const FILTER_INCLUDE = 2; - + /** - * @var int filters based on whether MimePart::isMultiPart is set + * @var int filters based on whether MessagePart::hasContent is true + */ + private $hascontent = PartFilter::FILTER_OFF; + + /** + * @var int filters based on whether MimePart::isMultiPart is true */ private $multipart = PartFilter::FILTER_OFF; /** - * @var int filters based on whether MimePart::isTextPart is set + * @var int filters based on whether MessagePart::isTextPart is true */ private $textpart = PartFilter::FILTER_OFF; @@ -73,6 +80,11 @@ class PartFilter */ private $signedpart = PartFilter::FILTER_EXCLUDE; + /** + * @var string calculated hash of the filter + */ + private $hashCode; + /** * @var string[][] array of header rules. The top-level contains keys of * FILTER_INCLUDE and/or FILTER_EXCLUDE, which contain key => value mapping @@ -86,16 +98,6 @@ class PartFilter * ``` */ private $headers = []; - - /** - * @var string[] map of headers and default values if the header isn't set. - * This allows text/plain to match a Content-Type header that hasn't - * been set for instance. - */ - private $defaultHeaderValues = [ - 'Content-Type' => 'text/plain', - 'Content-Disposition' => 'inline', - ]; /** * Convenience method to filter for a specific mime type. @@ -166,7 +168,7 @@ public static function fromDisposition($disposition, $multipart = PartFilter::FI */ public function __construct(array $filter = []) { - $params = [ 'multipart', 'textpart', 'signedpart', 'headers' ]; + $params = [ 'hascontent', 'multipart', 'textpart', 'signedpart', 'headers' ]; foreach ($params as $param) { if (isset($filter[$param])) { $this->__set($param, $filter[$param]); @@ -231,7 +233,8 @@ public function setHeaders(array $headers) */ public function __set($name, $value) { - if ($name === 'multipart' || $name === 'textpart' || $name === 'signedpart') { + if ($name === 'hascontent' || $name === 'multipart' + || $name === 'textpart' || $name === 'signedpart') { $this->validateArgument( $name, $value, @@ -268,49 +271,65 @@ public function __get($name) { return $this->$name; } + + /** + * Returns true if the passed MessagePart fails the filter's hascontent + * filter settings. + * + * @param MessagePart $part + * @return bool + */ + private function failsHasContentFilter(MessagePart $part) + { + return ($this->hascontent === static::FILTER_EXCLUDE && $part->hasContent()) + || ($this->hascontent === static::FILTER_INCLUDE && !$part->hasContent()); + } /** - * Returns true if the passed MimePart fails the filter's multipart filter + * Returns true if the passed MessagePart fails the filter's multipart filter * settings. * - * @param \ZBateson\MailMimeParser\Message\MimePart $part + * @param MessagePart $part * @return bool */ - private function failsMultiPartFilter(MimePart $part) + private function failsMultiPartFilter(MessagePart $part) { + if (!($part instanceof MimePart)) { + return $this->multipart !== static::FILTER_EXCLUDE; + } return ($this->multipart === static::FILTER_EXCLUDE && $part->isMultiPart()) || ($this->multipart === static::FILTER_INCLUDE && !$part->isMultiPart()); } /** - * Returns true if the passed MimePart fails the filter's textpart filter + * Returns true if the passed MessagePart fails the filter's textpart filter * settings. * - * @param \ZBateson\MailMimeParser\Message\MimePart $part + * @param MessagePart $part * @return bool */ - private function failsTextPartFilter(MimePart $part) + private function failsTextPartFilter(MessagePart $part) { return ($this->textpart === static::FILTER_EXCLUDE && $part->isTextPart()) || ($this->textpart === static::FILTER_INCLUDE && !$part->isTextPart()); } /** - * Returns true if the passed MimePart fails the filter's signedpart filter - * settings. + * Returns true if the passed MessagePart fails the filter's signedpart + * filter settings. * - * @param \ZBateson\MailMimeParser\Message\MimePart $part + * @param MessagePart $part * @return boolean */ - private function failsSignedPartFilter(MimePart $part) + private function failsSignedPartFilter(MessagePart $part) { if ($this->signedpart === static::FILTER_OFF) { return false; - } elseif ($part->getParent() === null) { + } elseif (!$part->isMime() || $part->getParent() === null) { return ($this->signedpart === static::FILTER_INCLUDE); } - $partMimeType = $part->getHeaderValue('Content-Type'); - $parentMimeType = $part->getParent()->getHeaderValue('Content-Type'); + $partMimeType = $part->getContentType(); + $parentMimeType = $part->getParent()->getContentType(); $parentProtocol = $part->getParent()->getHeaderParameter('Content-Type', 'protocol'); if (strcasecmp($parentMimeType, 'multipart/signed') === 0 && strcasecmp($partMimeType, $parentProtocol) === 0) { return ($this->signedpart === static::FILTER_EXCLUDE); @@ -318,21 +337,51 @@ private function failsSignedPartFilter(MimePart $part) return ($this->signedpart === static::FILTER_INCLUDE); } + /** + * Tests a single header value against $part, and returns true if the test + * fails. + * + * @staticvar array $map + * @param MimePart $part + * @param int $type + * @param string $name + * @param string $header + * @return boolean + */ + private function failsHeaderFor($part, $type, $name, $header) + { + $headerValue = null; + + static $map = [ + 'content-type' => 'getContentType', + 'content-disposition' => 'getContentDisposition', + 'content-transfer-encoding' => 'getContentTransferEncoding' + ]; + $lower = strtolower($name); + if (isset($map[$lower])) { + $headerValue = call_user_func([$part, $map[$lower]]); + } elseif (!($part instanceof MimePart)) { + return ($type === static::FILTER_INCLUDE); + } else { + $headerValue = $part->getHeaderValue($name); + } + + return (($type === static::FILTER_EXCLUDE && strcasecmp($headerValue, $header) === 0) + || ($type === static::FILTER_INCLUDE && strcasecmp($headerValue, $header) !== 0)); + } + /** * Returns true if the passed MimePart fails the filter's header filter * settings. * - * @param \ZBateson\MailMimeParser\Message\MimePart $part + * @param \ZBateson\MailMimeParser\Message\Part\MimePart $part * @return boolean */ - public function failsHeaderPartFilter(MimePart $part) + private function failsHeaderPartFilter(MessagePart $part) { foreach ($this->headers as $type => $values) { foreach ($values as $name => $header) { - $default = (isset($this->defaultHeaderValues[$name])) ? $this->defaultHeaderValues[$name] : null; - $headerValue = $part->getHeaderValue($name, $default); - if (($type === static::FILTER_EXCLUDE && strcasecmp($headerValue, $header) === 0) - || ($type === static::FILTER_INCLUDE && strcasecmp($headerValue, $header) !== 0)) { + if ($this->failsHeaderFor($part, $type, $name, $header)) { return true; } } @@ -345,10 +394,10 @@ public function failsHeaderPartFilter(MimePart $part) * MimePart passes all filter tests, true is returned. Otherwise false is * returned. * - * @param \ZBateson\MailMimeParser\Message\MimePart $part + * @param \ZBateson\MailMimeParser\Message\Part\MimePart $part * @return boolean */ - public function filter(MimePart $part) + public function filter(MessagePart $part) { return !($this->failsMultiPartFilter($part) || $this->failsTextPartFilter($part) diff --git a/src/Message/PartFilterFactory.php b/src/Message/PartFilterFactory.php new file mode 100644 index 00000000..ee27cc34 --- /dev/null +++ b/src/Message/PartFilterFactory.php @@ -0,0 +1,80 @@ +mode = $mode; - $this->filename = $filename; - - $this->setRawHeader( - 'Content-Type', - 'application/octet-stream; name="' . addcslashes($filename, '"') . '"' - ); - $this->setRawHeader( - 'Content-Disposition', - 'attachment; filename="' . addcslashes($filename, '"') . '"' - ); - $this->setRawHeader('Content-Transfer-Encoding', 'x-uuencode'); - } - - /** - * Returns the file mode included in the uuencoded header for this part. - * - * @return int - */ - public function getUnixFileMode() - { - return $this->mode; - } - - /** - * Returns the filename included in the uuencoded header for this part. - * - * @return string - */ - public function getFilename() - { - return $this->filename; - } -} diff --git a/src/Message/Writer/MessageWriter.php b/src/Message/Writer/MessageWriter.php deleted file mode 100644 index 3e555eec..00000000 --- a/src/Message/Writer/MessageWriter.php +++ /dev/null @@ -1,117 +0,0 @@ - 0) { - fwrite($handle, str_repeat("\r\n", $numLinesBefore)); - } - fwrite($handle, '--'); - fwrite($handle, $boundary); - if ($isEnd) { - fwrite($handle, "--\r\n"); - } else { - fwrite($handle, "\r\n"); - } - } - - /** - * Writes out headers and content for the passed MimePart, then loops over - * its child parts calling recursiveWriteParts on each part. - * - * @param MimePart $part the current part to write out - * @param resource $handle the handle to write out to - * @return bool true if the part had children (and ended with writing a - * boundary) - */ - protected function recursiveWriteParts(MimePart $part, $handle) - { - $this->writePartHeadersTo($part, $handle); - $this->writePartContentTo($part, $handle); - $ended = false; - $boundary = $part->getHeaderParameter('Content-Type', 'boundary'); - foreach ($part->getChildParts() as $i => $child) { - if ($boundary !== null) { - $numLines = ($i !== 0 && !$ended) ? 2 : (int) $ended; - $this->writeBoundary($handle, $boundary, $numLines, false); - } - $ended = $this->recursiveWriteParts($child, $handle); - } - if ($boundary !== null) { - $this->writeBoundary($handle, $boundary, ($ended) ? 1 : 2, true); - return true; - } - return false; - } - - /** - * Saves the message as a MIME message to the passed resource handle. - * - * @param Message $message - * @param resource $handle - */ - public function writeMessageTo(Message $message, $handle) - { - if ($message->isMime()) { - $this->recursiveWriteParts($message, $handle); - } else { - $this->writePartHeadersTo($message, $handle); - $this->writePartContentTo($message, $handle); - foreach ($message->getChildParts() as $i => $child) { - fwrite($handle, "\r\n\r\n"); - $this->writePartContentTo($child, $handle); - } - } - } - - /** - * Returns the content part of a signed message for a signature to be - * calculated on the message. - * - * @param Message $message - * @return string - */ - public function getSignableBody(Message $message) - { - $messagePart = $message->getChild(0); - if (!$message->isMime() || $messagePart === null) { - return null; - } - $handle = fopen('php://temp', 'r+'); - $ended = $this->recursiveWriteParts($messagePart, $handle); - rewind($handle); - $str = stream_get_contents($handle); - fclose($handle); - if (!$ended) { - $str .= "\r\n"; - } - return $str; - } -} diff --git a/src/Message/Writer/MessageWriterService.php b/src/Message/Writer/MessageWriterService.php deleted file mode 100644 index 7aa2ccd9..00000000 --- a/src/Message/Writer/MessageWriterService.php +++ /dev/null @@ -1,35 +0,0 @@ - 76, - 'line-break-chars' => "\r\n", - ]; - - /** - * @var array map of transfer-encoding types to registered stream filter - * names used in setTransferEncodingFilterOnStream - */ - private static $typeToEncodingMap = [ - 'quoted-printable' => 'mmp-convert.quoted-printable-encode', - 'base64' => 'mmp-convert.base64-encode', - 'x-uuencode' => 'mailmimeparser-uuencode', - 'x-uue' => 'mailmimeparser-uuencode', - 'uuencode' => 'mailmimeparser-uuencode', - 'uue' => 'mailmimeparser-uuencode', - ]; - - /** - * Returns the singleton instance for the class, instantiating it if not - * already created. - */ - public static function getInstance() - { - static $instances = []; - $class = get_called_class(); - if (!isset($instances[$class])) { - $instances[$class] = new static(); - } - return $instances[$class]; - } - - /** - * Writes out the headers of the passed MimePart and follows them with an - * empty line. - * - * @param MimePart $part - * @param resource $handle - */ - public function writePartHeadersTo(MimePart $part, $handle) - { - $headers = $part->getHeaders(); - foreach ($headers as $header) { - fwrite($handle, "$header\r\n"); - } - fwrite($handle, "\r\n"); - } - - /** - * Sets up a mailmimeparser-encode stream filter on the content resource - * handle of the passed MimePart if applicable and returns a reference to - * the filter. - * - * @param MimePart $part - * @return resource a reference to the appended stream filter or null - */ - private function setCharsetStreamFilterOnPartStream(MimePart $part) - { - $handle = $part->getContentResourceHandle(); - if ($part->isTextPart()) { - return stream_filter_append( - $handle, - 'mailmimeparser-encode', - STREAM_FILTER_READ, - [ - 'charset' => 'UTF-8', - 'to' => $part->getHeaderParameter( - 'Content-Type', - 'charset', - 'ISO-8859-1' - ) - ] - ); - } - return null; - } - - /** - * Appends a stream filter on the passed MimePart's content resource handle - * based on the type of encoding for the passed part. - * - * @param MimePart $part - * @param resource $handle - * @param StreamLeftover $leftovers - * @return resource the stream filter - */ - private function setTransferEncodingFilterOnStream(MimePart $part, $handle, StreamLeftover $leftovers) - { - $contentHandle = $part->getContentResourceHandle(); - $encoding = strtolower($part->getHeaderValue('Content-Transfer-Encoding')); - $params = array_merge(self::$defaultStreamFilterParams, [ - 'leftovers' => $leftovers, - 'filename' => $part->getHeaderParameter( - 'Content-Type', - 'name', - 'null' - ) - ]); - if (isset(self::$typeToEncodingMap[$encoding])) { - return stream_filter_append( - $contentHandle, - self::$typeToEncodingMap[$encoding], - STREAM_FILTER_READ, - $params - ); - } - return null; - } - - /** - * Trims out any starting and ending CRLF characters in the stream. - * - * @param string $read the read string, and where the result will be written - * to - * @param bool $first set to true if this is the first set of read - * characters from the stream (ltrims CRLF) - * @param string $lastChars contains any CRLF characters from the last $read - * line if it ended with a CRLF (because they're trimmed from the - * end, and get prepended to $read). - */ - private function trimTextBeforeCopying(&$read, &$first, &$lastChars) - { - if ($first) { - $first = false; - $read = ltrim($read, "\r\n"); - } - $read = $lastChars . $read; - $lastChars = ''; - $matches = null; - if (preg_match('/[\r\n]+$/', $read, $matches)) { - $lastChars = $matches[0]; - $read = rtrim($read, "\r\n"); - } - } - - /** - * Copies the content of the $fromHandle stream into the $toHandle stream, - * maintaining the current read position in $fromHandle. The passed - * MimePart is where $fromHandle originated after setting up filters on - * $fromHandle. - * - * @param MimePart $part - * @param resource $fromHandle - * @param resource $toHandle - */ - private function copyContentStream(MimePart $part, $fromHandle, $toHandle) - { - $pos = ftell($fromHandle); - rewind($fromHandle); - // changed from stream_copy_to_stream because hhvm seems to stop before - // end of file for some reason - $lastChars = ''; - $first = true; - while (!feof($fromHandle)) { - $read = fread($fromHandle, 1024); - if (strcasecmp($part->getHeaderValue('Content-Encoding'), '8bit') !== 0) { - $read = preg_replace('/\r\n|\r|\n/', "\r\n", $read); - } - if ($part->isTextPart()) { - $this->trimTextBeforeCopying($read, $first, $lastChars); - } - fwrite($toHandle, $read); - } - fseek($fromHandle, $pos); - } - - /** - * Writes out the content portion of the mime part based on the headers that - * are set on the part, taking care of character/content-transfer encoding. - * - * @param MimePart $part - * @param resource $handle - */ - public function writePartContentTo(MimePart $part, $handle) - { - $contentHandle = $part->getContentResourceHandle(); - if ($contentHandle !== null) { - - $filter = $this->setCharsetStreamFilterOnPartStream($part); - $leftovers = new StreamLeftover(); - $encodingFilter = $this->setTransferEncodingFilterOnStream( - $part, - $handle, - $leftovers - ); - $this->copyContentStream($part, $contentHandle, $handle); - - if ($encodingFilter !== null) { - fflush($handle); - stream_filter_remove($encodingFilter); - fwrite($handle, $leftovers->encodedValue); - } - if ($filter !== null) { - stream_filter_remove($filter); - } - } - } - - /** - * Writes out the MimePart to the passed resource. - * - * Takes care of character and content transfer encoding on the output based - * on what headers are set. - * - * @param MimePart $part - * @param resource $handle - */ - public function writePartTo(MimePart $part, $handle) - { - $this->writePartHeadersTo($part, $handle); - $this->writePartContentTo($part, $handle); - } -} diff --git a/src/SimpleDi.php b/src/SimpleDi.php index 549c8978..b3e12a9d 100644 --- a/src/SimpleDi.php +++ b/src/SimpleDi.php @@ -6,19 +6,16 @@ */ namespace ZBateson\MailMimeParser; -use ZBateson\MailMimeParser\Message\MessageParser; -use ZBateson\MailMimeParser\Message\MimePartFactory; -use ZBateson\MailMimeParser\Message\Writer\MessageWriterService; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; use ZBateson\MailMimeParser\Header\HeaderFactory; -use ZBateson\MailMimeParser\Stream\PartStream; -use ZBateson\MailMimeParser\Stream\UUDecodeStreamFilter; -use ZBateson\MailMimeParser\Stream\UUEncodeStreamFilter; -use ZBateson\MailMimeParser\Stream\CharsetStreamFilter; -use ZBateson\MailMimeParser\Stream\ConvertStreamFilter; -use ZBateson\MailMimeParser\Stream\Base64DecodeStreamFilter; -use ZBateson\MailMimeParser\Stream\Base64EncodeStreamFilter; -use ZBateson\MailMimeParser\Stream\Helper\CharsetConverter; +use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; +use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; +use ZBateson\MailMimeParser\Message\Helper\MessageHelperService; +use ZBateson\MailMimeParser\Message\MessageParser; +use ZBateson\MailMimeParser\Message\Part\Factory\PartBuilderFactory; +use ZBateson\MailMimeParser\Message\Part\Factory\PartFactoryService; +use ZBateson\MailMimeParser\Message\Part\Factory\PartStreamFilterManagerFactory; +use ZBateson\StreamDecorators\Util\CharsetConverter; /** * Dependency injection container for use by ZBateson\MailMimeParser - because a @@ -31,15 +28,24 @@ class SimpleDi { /** - * @var \ZBateson\MailMimeParser\Message\MimePartFactory singleton 'service' instance + * @var type */ - protected $partFactory; + protected $partBuilderFactory; /** - * @var \ZBateson\MailMimeParser\Stream\PartStreamRegistry singleton - * 'service' instance + * @var type */ - protected $partStreamRegistry; + protected $partFactoryService; + + /** + * @var type + */ + protected $partFilterFactory; + + /** + * @var type + */ + protected $partStreamFilterManagerFactory; /** * @var \ZBateson\MailMimeParser\Header\HeaderFactory singleton 'service' @@ -66,11 +72,11 @@ class SimpleDi protected $consumerService; /** - * @var \ZBateson\MailMimeParser\Message\Writer\MessageWriterService - * singleton 'service' instance for getting MimePartWriter and MessageWriter - * instances + * @var MessageHelperService Used to get MessageHelper singletons */ - protected $messageWriterService; + protected $messageHelperService; + + protected $streamFactory; /** * Constructs a SimpleDi - call singleton() to invoke @@ -117,68 +123,63 @@ protected function getInstance($var, $class) public function newMessageParser() { return new MessageParser( - $this->newMessage(), - $this->getPartFactory(), - $this->getPartStreamRegistry() + $this->getPartFactoryService(), + $this->getPartBuilderFactory() ); } /** - * Constructs and returns a new Message object. + * Returns a MessageHelperService instance. * - * @return \ZBateson\MailMimeParser\Message + * @return MessageHelperService */ - public function newMessage() + public function getMessageHelperService() { - return new Message( - $this->getHeaderFactory(), - $this->getMessageWriterService()->getMessageWriter(), - $this->getPartFactory() - ); - } - - /** - * Returns a MessageWriterService instance. - * - * @return MessageWriterService - */ - public function getMessageWriterService() - { - if ($this->messageWriterService === null) { - $this->messageWriterService = new MessageWriterService(); + if ($this->messageHelperService === null) { + $this->messageHelperService = new MessageHelperService( + $this->getPartBuilderFactory() + ); + $this->messageHelperService->setPartFactoryService( + $this->getPartFactoryService() + ); } - return $this->messageWriterService; + return $this->messageHelperService; } - /** - * Constructs and returns a new CharsetConverter object. - * - * @param string $fromCharset source charset - * @param string $toCharset destination charset - * @return \ZBateson\MailMimeParser\Stream\Helper\CharsetConverter - */ - public function newCharsetConverter($fromCharset, $toCharset) + public function getPartFilterFactory() { - return new CharsetConverter( - $fromCharset, - $toCharset + return $this->getInstance( + 'partFilterFactory', + __NAMESPACE__ . '\Message\PartFilterFactory' ); } /** - * Returns the part factory service instance. * - * @return \ZBateson\MailMimeParser\Message\MimePartFactory + * @return type */ - public function getPartFactory() + public function getPartFactoryService() { - if ($this->partFactory === null) { - $this->partFactory = new MimePartFactory( + if ($this->partFactoryService === null) { + $this->partFactoryService = new PartFactoryService( $this->getHeaderFactory(), - $this->getMessageWriterService() + $this->getPartFilterFactory(), + $this->getStreamFactory(), + $this->getPartStreamFilterManagerFactory(), + $this->getMessageHelperService() + ); + } + return $this->partFactoryService; + } + + public function getPartBuilderFactory() + { + if ($this->partBuilderFactory === null) { + $this->partBuilderFactory = new PartBuilderFactory( + $this->getHeaderFactory() ); } - return $this->partFactory; + return $this->partBuilderFactory; } /** @@ -193,19 +194,31 @@ public function getHeaderFactory() } return $this->headerFactory; } + + public function getStreamFactory() + { + return $this->getInstance( + 'streamFactory', + __NAMESPACE__ . '\Stream\StreamFactory' + ); + } - /** - * Returns the part stream registry service instance. The method also - * registers the stream extension by calling registerStreamExtensions. - * - * @return \ZBateson\MailMimeParser\Stream\PartStreamRegistry - */ - public function getPartStreamRegistry() + public function getPartStreamFilterManagerFactory() { - if ($this->partStreamRegistry === null) { - $this->registerStreamExtensions(); + if ($this->partStreamFilterManagerFactory === null) { + $this->partStreamFilterManagerFactory = new PartStreamFilterManagerFactory( + $this->getStreamFactory() + ); } - return $this->getInstance('partStreamRegistry', __NAMESPACE__ . '\Stream\PartStreamRegistry'); + return $this->getInstance( + 'partStreamFilterManagerFactory', + __NAMESPACE__ . '\Message\Part\PartStreamFilterManagerFactory' + ); + } + + public function getCharsetConverter() + { + return new CharsetConverter(); } /** @@ -215,7 +228,10 @@ public function getPartStreamRegistry() */ public function getHeaderPartFactory() { - return $this->getInstance('headerPartFactory', __NAMESPACE__ . '\Header\Part\HeaderPartFactory'); + if ($this->headerPartFactory === null) { + $this->headerPartFactory = new HeaderPartFactory($this->getCharsetConverter()); + } + return $this->headerPartFactory; } /** @@ -225,7 +241,10 @@ public function getHeaderPartFactory() */ public function getMimeLiteralPartFactory() { - return $this->getInstance('mimeLiteralPartFactory', __NAMESPACE__ . '\Header\Part\MimeLiteralPartFactory'); + if ($this->mimeLiteralPartFactory === null) { + $this->mimeLiteralPartFactory = new MimeLiteralPartFactory($this->getCharsetConverter()); + } + return $this->mimeLiteralPartFactory; } /** @@ -244,44 +263,4 @@ public function getConsumerService() return $this->consumerService; } - /** - * Registers stream extensions for PartStream and CharsetStreamFilter - * - * @see stream_filter_register - * @see stream_wrapper_register - */ - protected function registerStreamExtensions() - { - stream_filter_register(UUDecodeStreamFilter::STREAM_FILTER_NAME, __NAMESPACE__ . '\Stream\UUDecodeStreamFilter'); - stream_filter_register(UUEncodeStreamFilter::STREAM_FILTER_NAME, __NAMESPACE__ . '\Stream\UUEncodeStreamFilter'); - stream_filter_register(CharsetStreamFilter::STREAM_FILTER_NAME, __NAMESPACE__ . '\Stream\CharsetStreamFilter'); - stream_wrapper_register(PartStream::STREAM_WRAPPER_PROTOCOL, __NAMESPACE__ . '\Stream\PartStream'); - - // originally created for HHVM compatibility, but decided to use them - // instead of built-in stream filters for reliability -- it seems the - // built-in base64-decode and encode stream filter does pretty much the - // same thing as HHVM's -- it only works on smaller streams where the - // entire stream comes in a single buffer. - // In addition, in HHVM 3.15 there seems to be a problem registering - // 'convert.quoted-printable-decode/encode -- so to make things simple - // decided to use my version instead and name them mmp-convert.* - // In 3.18-3.20, it seems we're not able to overwrite 'convert.*' - // filters, so now they're all named mmp-convert.* - stream_filter_register( - 'mmp-convert.quoted-printable-decode', - __NAMESPACE__ . '\Stream\ConvertStreamFilter' - ); - stream_filter_register( - 'mmp-convert.quoted-printable-encode', - __NAMESPACE__ . '\Stream\ConvertStreamFilter' - ); - stream_filter_register( - Base64EncodeStreamFilter::STREAM_FILTER_NAME, - __NAMESPACE__ . '\Stream\Base64EncodeStreamFilter' - ); - stream_filter_register( - Base64DecodeStreamFilter::STREAM_FILTER_NAME, - __NAMESPACE__ . '\Stream\Base64DecodeStreamFilter' - ); - } } diff --git a/src/Stream/Base64DecodeStreamFilter.php b/src/Stream/Base64DecodeStreamFilter.php deleted file mode 100644 index 81368c13..00000000 --- a/src/Stream/Base64DecodeStreamFilter.php +++ /dev/null @@ -1,87 +0,0 @@ -leftover and prepended to the first element of the first line in - * the next call to getLines. - * - * @param object $bucket - * @return string[] - */ - private function getRawBytes($bucket) - { - $raw = preg_replace('/\s+/', '', $bucket->data); - if ($this->leftover !== '') { - $raw = $this->leftover . $raw; - $this->leftover = ''; - } - $nLeftover = strlen($raw) % 4; - if ($nLeftover !== 0) { - $this->leftover = substr($raw, -$nLeftover); - $raw = substr($raw, 0, -$nLeftover); - } - return $raw; - } - - /** - * Filter implementation converts encoding before returning PSFS_PASS_ON. - * - * @param resource $in - * @param resource $out - * @param int $consumed - * @param bool $closing - * @return int - */ - public function filter($in, $out, &$consumed, $closing) - { - while ($bucket = stream_bucket_make_writeable($in)) { - $bytes = $this->getRawBytes($bucket); - $nConsumed = strlen($bucket->data); - $consumed += $nConsumed; - $converted = base64_decode($bytes); - - // $this->stream is undocumented. It was found looking at HHVM's source code - // for its convert.iconv.* implementation in ConvertIconFilter and explained - // somewhat in this StackOverflow page: http://stackoverflow.com/a/31132646/335059 - // declaring a member variable called 'stream' breaks the PHP implementation (5.5.9 - // at least). - stream_bucket_append($out, stream_bucket_new($this->stream, $converted)); - } - return PSFS_PASS_ON; - } -} diff --git a/src/Stream/Base64EncodeStreamFilter.php b/src/Stream/Base64EncodeStreamFilter.php deleted file mode 100644 index 00eb3118..00000000 --- a/src/Stream/Base64EncodeStreamFilter.php +++ /dev/null @@ -1,110 +0,0 @@ -numBytesWritten != 0) { - $next = (76 - ($this->numBytesWritten % 76)) % 76; - $converted = substr($converted, 0, $next) . "\r\n" . rtrim(chunk_split(substr($converted, $next), 76)); - } else { - $converted = rtrim(chunk_split($converted)); - } - $this->numBytesWritten += $numBytes; - stream_bucket_append($out, stream_bucket_new($this->stream, $converted)); - } - - /** - * Reads from the input bucket stream, converts, and writes the uuencoded - * stream to $out. - * - * @param resource $in input bucket stream - * @param resource $out output bucket stream - * @param int $consumed incremented by number of bytes read from $in - */ - private function readAndConvert($in, $out, &$consumed) - { - while ($bucket = stream_bucket_make_writeable($in)) { - $data = $this->leftovers->value . $bucket->data; - $consumed += $bucket->datalen; - $nRemain = strlen($data) % 3; - $toConvert = $data; - if ($nRemain === 0) { - $this->leftovers->value = ''; - $this->leftovers->encodedValue = ''; - } else { - $this->leftovers->value = substr($data, -$nRemain); - $this->leftovers->encodedValue = base64_encode($this->leftovers->value); - $toConvert = substr($data, 0, -$nRemain); - } - $this->convertAndAppend($toConvert, $out); - } - } - - /** - * Filter implementation converts encoding before returning PSFS_PASS_ON. - * - * @param resource $in - * @param resource $out - * @param int $consumed - * @param bool $closing - * @return int - */ - public function filter($in, $out, &$consumed, $closing) - { - $this->readAndConvert($in, $out, $consumed); - return PSFS_PASS_ON; - } - - /** - * Sets up the leftovers object - */ - public function onCreate() - { - if (isset($this->params['leftovers'])) { - $this->leftovers = $this->params['leftovers']; - } else { - $this->leftovers = new StreamLeftover(); - } - } -} diff --git a/src/Stream/CharsetStreamFilter.php b/src/Stream/CharsetStreamFilter.php deleted file mode 100644 index 91daa63d..00000000 --- a/src/Stream/CharsetStreamFilter.php +++ /dev/null @@ -1,83 +0,0 @@ -converter->convert($bucket->data); - $consumed += strlen($bucket->data); - - // $this->stream is undocumented. It was found looking at HHVM's source code - // for its convert.iconv.* implementation in ConvertIconFilter and explained - // somewhat in this StackOverflow page: http://stackoverflow.com/a/31132646/335059 - // declaring a member variable called 'stream' breaks the PHP implementation (5.5.9 - // at least). - stream_bucket_append($out, stream_bucket_new($this->stream, $converted)); - } - return PSFS_PASS_ON; - } - - /** - * Overridden to extract the charset from the params array and check if the - * passed charset is supported or listed in the translation table in - * CharsetStreamFilter::translatedCharsets. - * - * Unfortunately __construct doesn't seem to be called for this class, so - * setting up 'availableCharsets' in the constructor doesn't work out. - */ - public function onCreate() - { - $charset = 'ISO-8859-1'; - $to = 'UTF-8'; - if (!empty($this->params['charset'])) { - $charset = $this->params['charset']; - } - if (!empty($this->params['to'])) { - $to = $this->params['to']; - } - - $di = SimpleDi::singleton(); - $this->converter = $di->newCharsetConverter($charset, $to); - } -} diff --git a/src/Stream/ConvertStreamFilter.php b/src/Stream/ConvertStreamFilter.php deleted file mode 100644 index 3b1eff31..00000000 --- a/src/Stream/ConvertStreamFilter.php +++ /dev/null @@ -1,101 +0,0 @@ -filtername, 12); - $aFilters = [ - 'quoted-printable-encode' => true, - 'quoted-printable-decode' => true, - ]; - if (!isset($aFilters[$name])) { - return false; - } - $this->fnFilterName = str_replace('-', '_', $name); - return true; - } - - /** - * Sets up a remainder of read bytes if one of the last two bytes - * read is an '=' since quoted_printable_decode wouldn't work if one - * read operation ends with "=3" and the next begins with "D" for - * example. - * - * @param string $data - */ - private function getFilteredBucket($data) - { - $ret = $this->leftover . $data; - if ($this->fnFilterName === 'quoted_printable_decode') { - $len = strlen($ret); - $eq = strrpos($ret, '='); - if (($eq !== false) && ($eq === $len - 1 || $eq === $len - 2)) { - $this->leftover = substr($ret, $eq); - $ret = substr($ret, 0, $eq); - } else { - $this->leftover = ''; - } - } - return $ret; - } - - /** - * Filter implementation converts calls the relevant encode/decode filter - * and chunk_split if needed, before returning PSFS_PASS_ON. - * - * @param resource $in - * @param resource $out - * @param int $consumed - * @param bool $closing - * @return int - */ - public function filter($in, $out, &$consumed, $closing) - { - while ($bucket = stream_bucket_make_writeable($in)) { - $filtered = $this->getFilteredBucket($bucket->data); - $data = call_user_func($this->fnFilterName, $filtered); - stream_bucket_append($out, stream_bucket_new($this->stream, $data)); - } - return PSFS_PASS_ON; - } -} \ No newline at end of file diff --git a/src/Stream/HeaderStream.php b/src/Stream/HeaderStream.php new file mode 100644 index 00000000..7c1d2d32 --- /dev/null +++ b/src/Stream/HeaderStream.php @@ -0,0 +1,73 @@ +part = $part; + } + + private function getPartHeadersArray() + { + if ($this->part instanceof ParentHeaderPart) { + return $this->part->getRawHeaders(); + } elseif ($this->part->getParent() !== null && $this->part->getParent()->isMime()) { + return [ + [ 'Content-Type', $this->part->getContentType() ], + [ 'Content-Disposition', $this->part->getContentDisposition() ], + [ 'Content-Transfer-Encoding', $this->part->getContentTransferEncoding() ] + ]; + } + return []; + } + + /** + * Writes out the headers of the passed part and follows them with an + * empty line. + * + * @param MimePart $part + * @param StreamInterface $stream + */ + public function writePartHeadersTo(StreamInterface $stream) + { + $headers = $this->getPartHeadersArray($this->part); + foreach ($headers as $header) { + $stream->write("${header[0]}: ${header[1]}\r\n"); + } + $stream->write("\r\n"); + } + + /** + * Creates the underlying stream lazily when required. + * + * @return StreamInterface + */ + protected function createStream() + { + $stream = Psr7\stream_for(); + $this->writePartHeadersTo($stream); + $stream->rewind(); + return $stream; + } +} diff --git a/src/Stream/Helper/CharsetConverter.php b/src/Stream/Helper/CharsetConverter.php deleted file mode 100644 index c554007c..00000000 --- a/src/Stream/Helper/CharsetConverter.php +++ /dev/null @@ -1,408 +0,0 @@ - //opensource.org/licenses/bsd-license.php BSD - */ -namespace ZBateson\MailMimeParser\Stream\Helper; - -/** - * Helper class for converting strings between charsets. - * - * CharasetConverter tries to convert using mb_convert_encoding when possible, - * defining as many aliases as possible for supported encodings. If not - * supported, iconv is attempted. - * - * @author Zaahid Bateson - */ -class CharsetConverter -{ - /** - * @var array aliased charsets supported by mb_convert_encoding. - * The alias is stripped of any non-alphanumeric characters (so CP367 - * is equal to CP-367) when comparing. - * Some of these translations are already supported by - * mb_convert_encoding on "my" PHP 5.5.9, but may not be supported in - * other implementations or versions since they're not part of - * documented support. - */ - public static $mbAliases = [ - // supported but not included in mb_list_encodings for some reason... - 'CP850' => 'CP850', - 'GB2312' => 'GB2312', - // aliases - '646' => 'ASCII', - 'ANSIX341968' => 'ASCII', - 'ANSIX341986' => 'ASCII', - 'CP367' => 'ASCII', - 'CSASCII' => 'ASCII', - 'IBM367' => 'ASCII', - 'ISO646US' => 'ASCII', - 'ISO646IRV1991' => 'ASCII', - 'ISOIR6' => 'ASCII', - 'US' => 'ASCII', - 'USASCII' => 'ASCII', - 'BIG5' => 'BIG-5', - 'BIG5TW' => 'BIG-5', - 'CSBIG5' => 'BIG-5', - '1251' => 'WINDOWS-1251', - 'CP1251' => 'WINDOWS-1251', - 'WINDOWS1251' => 'WINDOWS-1251', - '1252' => 'WINDOWS-1252', - 'CP1252' => 'WINDOWS-1252', - 'WINDOWS1252' => 'WINDOWS-1252', - 'WE8MSWIN1252' => 'WINDOWS-1252', - '1254' => 'WINDOWS-1254', - 'CP1254' => 'WINDOWS-1254', - 'WINDOWS1254' => 'WINDOWS-1254', - '1255' => 'ISO-8859-8', - 'CP1255' => 'ISO-8859-8', - 'ISO88598I' => 'ISO-8859-8', - 'WINDOWS1255' => 'ISO-8859-8', - '850' => 'CP850', - 'CSPC850MULTILINGUAL' => 'CP850', - 'IBM850' => 'CP850', - '866' => 'CP866', - 'CSIBM866' => 'CP866', - 'IBM866' => 'CP866', - '932' => 'CP932', - 'MS932' => 'CP932', - 'MSKANJI' => 'CP932', - '950' => 'CP950', - 'MS950' => 'CP950', - 'EUCJP' => 'EUC-JP', - 'UJIS' => 'EUC-JP', - 'EUCKR' => 'EUC-KR', - 'KOREAN' => 'EUC-KR', - 'KSC5601' => 'EUC-KR', - 'KSC56011987' => 'EUC-KR', - 'KSX1001' => 'EUC-KR', - 'GB180302000' => 'GB18030', - // GB2312 not listed but supported - 'CHINESE' => 'GB2312', - 'CSISO58GB231280' => 'GB2312', - 'EUCCN' => 'GB2312', - 'EUCGB2312CN' => 'GB2312', - 'GB23121980' => 'GB2312', - 'GB231280' => 'GB2312', - 'ISOIR58' => 'GB2312', - 'GBK' => 'CP936', - '936' => 'CP936', - 'ms936' => 'CP936', - 'HZGB' => 'HZ', - 'HZGB2312' => 'HZ', - 'CSISO2022JP' => 'ISO-2022-JP', - 'ISO2022JP' => 'ISO-2022-JP', - 'ISO2022JP2004' => 'ISO-2022-JP-2004', - 'CSISO2022KR' => 'ISO-2022-KR', - 'ISO2022KR' => 'ISO-2022-KR', - 'CSISOLATIN6' => 'ISO-8859-10', - 'ISO885910' => 'ISO-8859-10', - 'ISO8859101992' => 'ISO-8859-10', - 'ISOIR157' => 'ISO-8859-10', - 'L6' => 'ISO-8859-10', - 'LATIN6' => 'ISO-8859-10', - 'ISO885913' => 'ISO-8859-13', - 'ISO885914' => 'ISO-8859-14', - 'ISO8859141998' => 'ISO-8859-14', - 'ISOCELTIC' => 'ISO-8859-14', - 'ISOIR199' => 'ISO-8859-14', - 'L8' => 'ISO-8859-14', - 'LATIN8' => 'ISO-8859-14', - 'ISO885915' => 'ISO-8859-15', - 'ISO885916' => 'ISO-8859-16', - 'ISO8859162001' => 'ISO-8859-16', - 'ISOIR226' => 'ISO-8859-16', - 'L10' => 'ISO-8859-16', - 'LATIN10' => 'ISO-8859-16', - 'CSISOLATIN2' => 'ISO-8859-2', - 'ISO88592' => 'ISO-8859-2', - 'ISO885921987' => 'ISO-8859-2', - 'ISOIR101' => 'ISO-8859-2', - 'L2' => 'ISO-8859-2', - 'LATIN2' => 'ISO-8859-2', - 'CSISOLATIN3' => 'ISO-8859-3', - 'ISO88593' => 'ISO-8859-3', - 'ISO885931988' => 'ISO-8859-3', - 'ISOIR109' => 'ISO-8859-3', - 'L3' => 'ISO-8859-3', - 'LATIN3' => 'ISO-8859-3', - 'CSISOLATIN4' => 'ISO-8859-4', - 'ISO88594' => 'ISO-8859-4', - 'ISO885941988' => 'ISO-8859-4', - 'ISOIR110' => 'ISO-8859-4', - 'L4' => 'ISO-8859-4', - 'LATIN4' => 'ISO-8859-4', - 'CSISOLATINCYRILLIC' => 'ISO-8859-5', - 'CYRILLIC' => 'ISO-8859-5', - 'ISO88595' => 'ISO-8859-5', - 'ISO885951988' => 'ISO-8859-5', - 'ISOIR144' => 'ISO-8859-5', - 'ARABIC' => 'ISO-8859-6', - 'ASMO708' => 'ISO-8859-6', - 'CSISOLATINARABIC' => 'ISO-8859-6', - 'ECMA114' => 'ISO-8859-6', - 'ISO88596' => 'ISO-8859-6', - 'ISO885961987' => 'ISO-8859-6', - 'ISOIR127' => 'ISO-8859-6', - 'CSISOLATINGREEK' => 'ISO-8859-7', - 'ECMA118' => 'ISO-8859-7', - 'ELOT928' => 'ISO-8859-7', - 'GREEK' => 'ISO-8859-7', - 'GREEK8' => 'ISO-8859-7', - 'ISO88597' => 'ISO-8859-7', - 'ISO885971987' => 'ISO-8859-7', - 'ISOIR126' => 'ISO-8859-7', - 'CSISOLATINHEBREW' => 'ISO-8859-8', - 'HEBREW' => 'ISO-8859-8', - 'ISO88598' => 'ISO-8859-8', - 'ISO885981988' => 'ISO-8859-8', - 'ISOIR138' => 'ISO-8859-8', - 'CSISOLATIN5' => 'ISO-8859-9', - 'ISO88599' => 'ISO-8859-9', - 'ISO885991989' => 'ISO-8859-9', - 'ISOIR148' => 'ISO-8859-9', - 'L5' => 'ISO-8859-9', - 'LATIN5' => 'ISO-8859-9', - 'CSKOI8R' => 'KOI8-R', - 'KOI8R' => 'KOI8-R', - '8859' => 'ISO-8859-1', - 'CP819' => 'ISO-8859-1', - 'CSISOLATIN1' => 'ISO-8859-1', - 'IBM819' => 'ISO-8859-1', - 'ISO8859' => 'ISO-8859-1', - 'ISO88591' => 'ISO-8859-1', - 'ISO885911987' => 'ISO-8859-1', - 'ISOIR100' => 'ISO-8859-1', - 'L1' => 'ISO-8859-1', - 'LATIN' => 'ISO-8859-1', - 'LATIN1' => 'ISO-8859-1', - 'CSSHIFTJIS' => 'SJIS', - 'SHIFTJIS' => 'SJIS', - 'SHIFTJIS2004' => 'SJIS-2004', - 'SJIS2004' => 'SJIS-2004', - ]; - - /** - * @var array aliased charsets supported by iconv. - */ - public static $iconvAliases = [ - // iconv aliases -- a lot of these may already be supported - 'BIG5HKSCS' => 'BIG5HKSCS', - 'HKSCS' => 'BIG5HKSCS', - '037' => 'CP037', - 'EBCDICCPCA' => 'CP037', - 'EBCDICCPNL' => 'CP037', - 'EBCDICCPUS' => 'CP037', - 'EBCDICCPWT' => 'CP037', - 'CSIBM037' => 'CP037', - 'IBM037' => 'CP037', - 'IBM039' => 'CP037', - '1026' => 'CP1026', - 'CSIBM1026' => 'CP1026', - 'IBM1026' => 'CP1026', - '1140' => 'CP1140', - 'IBM1140' => 'CP1140', - '1250' => 'CP1250', - 'WINDOWS1250' => 'CP1250', - '1253' => 'CP1253', - 'WINDOWS1253' => 'CP1253', - '1256' => 'CP1256', - 'WINDOWS1256' => 'CP1256', - '1257' => 'CP1257', - 'WINDOWS1257' => 'CP1257', - '1258' => 'CP1258', - 'WINDOWS1258' => 'CP1258', - '424' => 'CP424', - 'CSIBM424' => 'CP424', - 'EBCDICCPHE' => 'CP424', - 'IBM424' => 'CP424', - '437' => 'CP437', - 'CSPC8CODEPAGE437' => 'CP437', - 'IBM437' => 'CP437', - '500' => 'CP500', - 'CSIBM500' => 'CP500', - 'EBCDICCPBE' => 'CP500', - 'EBCDICCPCH' => 'CP500', - 'IBM500' => 'CP500', - '775' => 'CP775', - 'CSPC775BALTIC' => 'CP775', - 'IBM775' => 'CP775', - '860' => 'CP860', - 'CSIBM860' => 'CP860', - 'IBM860' => 'CP860', - '861' => 'CP861', - 'CPIS' => 'CP861', - 'CSIBM861' => 'CP861', - 'IBM861' => 'CP861', - '862' => 'CP862', - 'CSPC862LATINHEBREW' => 'CP862', - 'IBM862' => 'CP862', - '863' => 'CP863', - 'CSIBM863' => 'CP863', - 'IBM863' => 'CP863', - '864' => 'CP864', - 'CSIBM864' => 'CP864', - 'IBM864' => 'CP864', - '865' => 'CP865', - 'CSIBM865' => 'CP865', - 'IBM865' => 'CP865', - '869' => 'CP869', - 'CPGR' => 'CP869', - 'CSIBM869' => 'CP869', - 'IBM869' => 'CP869', - '949' => 'CP949', - 'MS949' => 'CP949', - 'UHC' => 'CP949', - 'ROMAN8' => 'ROMAN8', - 'HPROMAN8' => 'ROMAN8', - 'R8' => 'ROMAN8', - 'CSHPROMAN8' => 'ROMAN8', - 'ISO2022JP2' => 'ISO2022JP2', - 'THAI' => 'ISO885911', - 'ISO885911' => 'ISO885911', - 'ISO8859112001' => 'ISO885911', - 'JOHAB' => 'CP1361', - 'MS1361' => 'CP1361', - 'MACCYRILLIC' => 'MACCYRILLIC', - 'CSPTCP154' => 'PT154', - 'PTCP154' => 'PT154', - 'CP154' => 'PT154', - 'CYRILLICASIAN' => 'PT154', - 'TIS620' => 'TIS620', - 'TIS6200' => 'TIS620', - 'TIS62025290' => 'TIS620', - 'TIS62025291' => 'TIS620', - 'ISOIR166' => 'TIS620', - ]; - - /** - * @var string charset to convert from - */ - protected $fromCharset; - - /** - * @var string charset to convert to - */ - protected $toCharset; - - /** - * @var boolean indicates if $fromCharset is supported by - * mb_convert_encoding - */ - protected $fromCharsetMbSupported = true; - - /** - * @var boolean indicates if $toCharset is supported by mb_convert_encoding - */ - protected $toCharsetMbSupported = true; - - /** - * Constructs the charset converter with source/destination charsets. - * - * @param string $fromCharset - * @param string $toCharset - */ - public function __construct($fromCharset, $toCharset) - { - $this->fromCharset = $this->findSupportedCharset($fromCharset, $this->fromCharsetMbSupported); - $this->toCharset = $this->findSupportedCharset($toCharset, $this->toCharsetMbSupported); - } - - /** - * Converts the passed string's charset from $this->fromCharset to - * $this->toCharset. - * - * The function attempts to use mb_convert_encoding if possible, and falls - * back to iconv if not. If the source or destination character sets aren't - * supported, a blank string is returned. - * - * @param string $str - * @return string - */ - public function convert($str) - { - // there may be some mb-supported encodings not supported by iconv (on my libiconv for instance - // HZ isn't supported), and so it may happen that failing an mb_convert_encoding, an iconv - // may also fail even though both support an encoding separately. - // Unfortunately there's no great way of testing what charsets are available on iconv, and - // attempting to blindly convert the string may be too costly, as could converting first - // to an intermediate (ASSUMPTION: may be worth testing converting to an intermediate) - if ($str !== '') { - if ($this->fromCharsetMbSupported && $this->toCharsetMbSupported) { - return mb_convert_encoding($str, $this->toCharset, $this->fromCharset); - } - return iconv($this->fromCharset, $this->toCharset . '//TRANSLIT//IGNORE', $str); - } - return $str; - } - - /** - * Looks up the passed $cs in mb_list_encodings, then strips non - * alpha-numeric characters and tries again, then failing that calls - * findAliasedCharset. The method returns the charset name that should be - * used in calls to mb_convert_encoding or iconv. - * - * If the charset is part of mb_list_encodings, $mbSupported is set to true. - * - * @param string $cs - * @param boolean $mbSupported - * @return string the final charset name to use - */ - private function findSupportedCharset($cs, &$mbSupported) - { - $mbSupported = true; - $comp = strtoupper($cs); - $available = array_map('strtoupper', mb_list_encodings()); - if (in_array($comp, $available)) { - return $comp; - } - $stripped = preg_replace('/[^A-Z0-9]+/', '', $comp); - if (in_array($stripped, $available)) { - return $stripped; - } - return $this->findAliasedCharset($comp, $stripped, $mbSupported); - } - - /** - * Looks up the passed $comp and $stripped strings in self::$mbAliases, and - * returns the mapped charset if applicable. Otherwise calls - * $this->findAliasedIconvCharset. - * - * $mbSupported is set to false if the charset is not located in - * self::$mbAliases. - * - * @param string $comp - * @param string $stripped - * @param boolean $mbSupported - * @return string the mapped charset - */ - private function findAliasedCharset($comp, $stripped, &$mbSupported) - { - if (array_key_exists($comp, self::$mbAliases)) { - return self::$mbAliases[$comp]; - } elseif (array_key_exists($stripped, self::$mbAliases)) { - return self::$mbAliases[$stripped]; - } - $mbSupported = false; - return $this->findAliasedIconvCharset($comp, $stripped); - } - - /** - * Looks up the passed $comp and $stripped strings in self::$iconvAliases, - * and returns the mapped charset if applicable. Otherwise returns $comp. - * - * @param string $comp - * @param string $stripped - * @return string the mapped charset (if mapped) or $comp otherwise - */ - private function findAliasedIconvCharset($comp, $stripped) - { - if (array_key_exists($comp, self::$iconvAliases)) { - return static::$iconvAliases[$comp]; - } elseif (array_key_exists($stripped, self::$iconvAliases)) { - return static::$iconvAliases[$stripped]; - } - return $comp; - } -} diff --git a/src/Stream/MessagePartStream.php b/src/Stream/MessagePartStream.php new file mode 100644 index 00000000..c47f2dac --- /dev/null +++ b/src/Stream/MessagePartStream.php @@ -0,0 +1,160 @@ +streamFactory = $sdf; + $this->part = $part; + } + + /** + * Sets up a mailmimeparser-encode stream filter on the content resource + * handle of the passed MimePart if applicable and returns a reference to + * the filter. + * + * @param MimePart $part + * @return StreamInterface a reference to the appended stream filter or null + */ + private function getCharsetDecoratorForStream(MessagePart $part, StreamInterface $stream) + { + $charset = $part->getCharset(); + if (!empty($charset)) { + $decorator = $this->streamFactory->newCharsetStream( + $stream, + $charset, + MailMimeParser::DEFAULT_CHARSET + ); + return $decorator; + } + return $stream; + } + + /** + * Appends a stream filter on the passed MimePart's content resource handle + * based on the type of encoding for the passed part. + * + * @param MimePart $part + * @param resource $handle + * @param StreamLeftover $leftovers + * @return StreamInterface the stream filter + */ + private function getTransferEncodingDecoratorForStream( + MessagePart $part, + StreamInterface $stream + ) { + $encoding = $part->getContentTransferEncoding(); + $decorator = null; + switch ($encoding) { + case 'quoted-printable': + $decorator = $this->streamFactory->newQuotedPrintableStream($stream); + break; + case 'base64': + $decorator = $this->streamFactory->newBase64Stream( + $this->streamFactory->newChunkSplitStream($stream)); + break; + case 'x-uuencode': + $decorator = $this->streamFactory->newUUStream($stream); + $decorator->setFilename($part->getFilename()); + break; + default: + return $stream; + } + return $decorator; + } + + /** + * Writes out the content portion of the mime part based on the headers that + * are set on the part, taking care of character/content-transfer encoding. + * + * @param MessagePart $part + * @param StreamInterface $stream + */ + public function writePartContentTo(MessagePart $part, StreamInterface $stream) + { + $contentStream = $part->getContentStream(); + if ($contentStream !== null) { + $copyStream = $this->streamFactory->newNonClosingStream($stream); + $es = $this->getTransferEncodingDecoratorForStream( + $part, + $copyStream + ); + $cs = $this->getCharsetDecoratorForStream($part, $es); + Psr7\copy_to_stream($contentStream, $cs); + $cs->close(); + } + } + + protected function getBoundaryAndChildStreams(ParentHeaderPart $part) + { + $streams = []; + $boundary = $part->getHeaderParameter('Content-Type', 'boundary'); + foreach ($part->getChildParts() as $i => $child) { + if ($boundary !== null) { + if ($i === 0 && !$part->hasContent()) { + $streams[] = Psr7\stream_for("--$boundary\r\n"); + } else { + $streams[] = Psr7\stream_for("\r\n--$boundary\r\n"); + } + } + $streams[] = $child->getStream(); + } + if ($boundary !== null) { + $streams[] = Psr7\stream_for("\r\n--$boundary--\r\n"); + } + return $streams; + } + + protected function getStreamsArray() + { + $content = Psr7\stream_for(); + $this->writePartContentTo($this->part, $content); + $content->rewind(); + $streams = [ new HeaderStream($this->part), $content ]; + + if ($this->part instanceof ParentHeaderPart) { + $streams = array_merge($streams, $this->getBoundaryAndChildStreams($this->part)); + } + + return $streams; + } + + /** + * Creates the underlying stream lazily when required. + * + * @return StreamInterface + */ + protected function createStream() + { + return new AppendStream($this->getStreamsArray()); + } +} diff --git a/src/Stream/PartStream.php b/src/Stream/PartStream.php deleted file mode 100644 index 4450c4f6..00000000 --- a/src/Stream/PartStream.php +++ /dev/null @@ -1,238 +0,0 @@ -handle where the current - * mime part's content starts. - */ - protected $start; - - /** - * @var int The offset character position in $this->handle where the current - * mime part's content ends. - */ - protected $end; - - /** - * @var PartStreamRegistry The registry service object. - */ - protected $registry; - - /** - * @var int the current read position. - */ - private $position; - - /** - * Constructs a PartStream. - */ - public function __construct() - { - $di = SimpleDi::singleton(); - $this->registry = $di->getPartStreamRegistry(); - } - - /** - * Extracts the PartStreamRegistry resource id, start, and end positions for - * the passed path and assigns them to the passed-by-reference parameters - * $id, $start and $end respectively. - * - * @param string $path - * @param string $id - * @param int $start - * @param int $end - */ - private function parseOpenPath($path, &$id, &$start, &$end) - { - $vars = []; - $parts = parse_url($path); - if (!empty($parts['host']) && !empty($parts['query'])) { - parse_str($parts['query'], $vars); - $id = $parts['host']; - $start = intval($vars['start']); - $end = intval($vars['end']); - } - } - - /** - * Called in response to fopen, file_get_contents, etc... with a - * PartStream::STREAM_WRAPPER_PROTOCOL, e.g., - * fopen('mmp-mime-message://...'); - * - * The \ZBateson\MailMimeParser\Message object ID must be passed as the - * 'host' part in $path. The start and end boundaries of the part must be - * passed as query string parameters in the path, for example: - * - * fopen('mmp-mime-message://123456?start=0&end=20'); - * - * This would open a file handle to a MIME message with the ID 123456, with - * a start offset of 0, and an end offset of 20. - * - * TODO: $mode is not validated, although only read operations are - * implemented in PartStream. $options are not checked for error reporting - * mode. - * - * @param string $path The requested path - * @param string $mode The requested open mode - * @param int $options Additional streams API flags - * @param string $opened_path The full path of the opened resource - * @return boolean true if the resource was opened successfully - */ - public function stream_open($path, $mode, $options, &$opened_path) - { - $this->position = 0; - $this->parseOpenPath($path, $this->id, $this->start, $this->end); - $this->handle = $this->registry->get($this->id); - $this->registry->increaseHandleRefCount($this->id); - return ($this->handle !== null && $this->start !== null && $this->end !== null); - } - - /** - * Decreases the ref count for the underlying resource handle, which allows - * the PartStreamRegistry to close it once no more references to it exist. - */ - public function stream_close() - { - $this->registry->decreaseHandleRefCount($this->id); - } - - /** - * Reads up to $count characters from the stream and returns them. - * - * @param int $count - * @return string - */ - public function stream_read($count) - { - $pos = ftell($this->handle); - fseek($this->handle, $this->start + $this->position); - $max = $this->end - ($this->start + $this->position); - $nRead = min($count, $max); - $ret = ''; - if ($nRead > 0) { - $ret = fread($this->handle, $nRead); - } - $this->position += strlen($ret); - fseek($this->handle, $pos); - return $ret; - } - - /** - * Returns the current read position. - * - * @return int - */ - public function stream_tell() - { - return $this->position; - } - - /** - * Returns true if the end of the stream has been reached. - * - * @return boolean - */ - public function stream_eof() - { - if ($this->position + $this->start >= $this->end) { - return true; - } - return false; - } - - /** - * Checks if the position is valid and seeks to it by setting - * $this->position - * - * @param int $pos - * @return boolean true if set - */ - private function streamSeekSet($pos) - { - if ($pos + $this->start < $this->end && $pos >= 0) { - $this->position = $pos; - return true; - } - return false; - } - - /** - * Moves the pointer to the given offset, in accordance to $whence. - * - * @param int $offset - * @param int $whence One of SEEK_SET, SEEK_CUR and SEEK_END. - * @return boolean - */ - public function stream_seek($offset, $whence = SEEK_SET) - { - $pos = $offset; - switch ($whence) { - case SEEK_CUR: - // @codeCoverageIgnoreStart - // this seems to be calculated for me in my version of PHP (5.5.9) - $pos = $this->position + $offset; - break; - // @codeCoverageIgnoreEnd - case SEEK_END: - $pos = ($this->end - $this->start) + $offset; - break; - default: - break; - } - return $this->streamSeekSet($pos); - } - - /** - * Returns information about the opened stream, as would be expected by - * fstat. - * - * @return array - */ - public function stream_stat() - { - $arr = fstat($this->handle); - if (!empty($arr['size'])) { - $arr['size'] = $this->end - $this->start; - } - return $arr; - } -} diff --git a/src/Stream/PartStreamRegistry.php b/src/Stream/PartStreamRegistry.php deleted file mode 100644 index 874030e2..00000000 --- a/src/Stream/PartStreamRegistry.php +++ /dev/null @@ -1,193 +0,0 @@ -registeredHandles[$id])) { - $this->registeredHandles[$id] = $handle; - $this->numRefCountsForHandles[$id] = 0; - } - } - - /** - * Unregisters the given message ID and closes the associated resource - * handle. - * - * @param string $id - */ - protected function unregister($id) - { - fclose($this->registeredHandles[$id]); - unset($this->registeredHandles[$id], $this->registeredPartStreamHandles[$id]); - } - - /** - * Increases the reference count for streams using the resource handle - * associated with the message id. - * - * @param int $messageId - */ - public function increaseHandleRefCount($messageId) - { - $this->numRefCountsForHandles[$messageId] += 1; - } - - /** - * Decreases the reference count for streams using the resource handle - * associated with the message id. Once the reference count hits 0, - * unregister is called. - * - * @param int $messageId - */ - public function decreaseHandleRefCount($messageId) - { - $this->numRefCountsForHandles[$messageId] -= 1; - if ($this->numRefCountsForHandles[$messageId] === 0) { - $this->unregister($messageId); - } - } - - /** - * Returns the resource handle with the passed $id. - * - * @param string $id - * @return resource - */ - public function get($id) - { - if (!isset($this->registeredHandles[$id])) { - return null; - } - return $this->registeredHandles[$id]; - } - - /** - * Attaches a stream filter on the passed resource $handle for the part's - * encoding. - * - * @param \ZBateson\MailMimeParser\Message\MimePart $part - * @param resource $handle - */ - private function attachEncodingFilterToStream(MimePart $part, $handle) - { - $encoding = strtolower($part->getHeaderValue('Content-Transfer-Encoding')); - switch ($encoding) { - case 'quoted-printable': - stream_filter_append($handle, 'mmp-convert.quoted-printable-decode', STREAM_FILTER_READ); - break; - case 'base64': - stream_filter_append($handle, 'mmp-convert.base64-decode', STREAM_FILTER_READ); - break; - case 'x-uuencode': - case 'x-uue': - case 'uuencode': - case 'uue': - stream_filter_append($handle, 'mailmimeparser-uudecode', STREAM_FILTER_READ); - break; - default: - break; - } - } - - /** - * Attaches a mailmimeparser-encode stream filter based on the part's - * defined charset. - * - * @param \ZBateson\MailMimeParser\Message\MimePart $part - * @param resource $handle - */ - private function attachCharsetFilterToStream(MimePart $part, $handle) - { - if ($part->isTextPart()) { - stream_filter_append( - $handle, - 'mailmimeparser-encode', - STREAM_FILTER_READ, - [ 'charset' => $part->getHeaderParameter('Content-Type', 'charset') ] - ); - } - } - - /** - * Creates a part stream handle for the start and end position of the - * message stream, and attaches it to the passed MimePart. - * - * @param MimePart $part - * @param Message $message - * @param int $start - * @param int $end - */ - public function attachContentPartStreamHandle(MimePart $part, Message $message, $start, $end) - { - $id = $message->getObjectId(); - if (empty($this->registeredHandles[$id])) { - return null; - } - $handle = fopen('mmp-mime-message://' . $id . '?start=' . - $start . '&end=' . $end, 'r'); - - $this->attachEncodingFilterToStream($part, $handle); - $this->attachCharsetFilterToStream($part, $handle); - $part->attachContentResourceHandle($handle); - } - - /** - * Creates a part stream handle for the start and end position of the - * message stream, and attaches it to the passed MimePart. - * - * @param MimePart $part - * @param Message $message - * @param int $start - * @param int $end - */ - public function attachOriginalPartStreamHandle(MimePart $part, Message $message, $start, $end) - { - $id = $message->getObjectId(); - if (empty($this->registeredHandles[$id])) { - return null; - } - $handle = fopen('mmp-mime-message://' . $id . '?start=' . - $start . '&end=' . $end, 'r'); - - $part->attachOriginalStreamHandle($handle); - } -} diff --git a/src/Stream/StreamFactory.php b/src/Stream/StreamFactory.php new file mode 100644 index 00000000..7a360c09 --- /dev/null +++ b/src/Stream/StreamFactory.php @@ -0,0 +1,95 @@ +newLimitStream( + $stream, + $part->getStreamPartLength(), + $part->getStreamPartStartOffset() + ); + } + + public function getLimitedContentStream(StreamInterface $stream, PartBuilder $part) + { + $length = $part->getStreamContentLength(); + if ($length !== 0) { + return $this->newLimitStream( + $stream, + $part->getStreamContentLength(), + $part->getStreamContentStartOffset() + ); + } + return null; + } + + private function newLimitStream(StreamInterface $stream, $length, $start) + { + return new SeekingLimitStream( + $this->newNonClosingStream($stream), + $length, + $start + ); + } + + public function newNonClosingStream(StreamInterface $stream) + { + return new NonClosingStream($stream); + } + + public function newChunkSplitStream(StreamInterface $stream) + { + return new ChunkSplitStream($stream); + } + + public function newBase64Stream(StreamInterface $stream) + { + return new Base64Stream( + new PregReplaceFilterStream($stream, '/[^a-zA-Z0-9\/\+=]/', '') + ); + } + + public function newQuotedPrintableStream(StreamInterface $stream) + { + return new QuotedPrintableStream($stream); + } + + public function newUUStream(StreamInterface $stream) + { + return new UUStream($stream); + } + + public function newCharsetStream(StreamInterface $stream, $fromCharset, $toCharset) + { + return new CharsetStream($stream, $fromCharset, $toCharset); + } + + public function newMessagePartStream(MessagePart $part) + { + return new MessagePartStream($this, $part); + } +} diff --git a/src/Stream/StreamLeftover.php b/src/Stream/StreamLeftover.php deleted file mode 100644 index 0c2f5471..00000000 --- a/src/Stream/StreamLeftover.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ -class StreamLeftover -{ - public $value = ''; - public $encodedValue = ''; -} diff --git a/src/Stream/UUDecodeStreamFilter.php b/src/Stream/UUDecodeStreamFilter.php deleted file mode 100644 index 871835e1..00000000 --- a/src/Stream/UUDecodeStreamFilter.php +++ /dev/null @@ -1,147 +0,0 @@ -leftover and prepended to the first element of the first line in - * the next call to getLines. - * - * @param object $bucket - * @return string[] - */ - private function getLines($bucket) - { - $lines = preg_split( - '/([^\r\n]+[\r\n]+)/', - $bucket->data, - -1, - PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY - ); - if ($this->leftover !== '') { - $lines[0] = $this->leftover . $lines[0]; - $this->leftover = ''; - } - $last = end($lines); - if ($last[strlen($last) - 1] !== "\n") { - $this->leftover = array_pop($lines); - } - return $lines; - } - - /** - * Returns true if the passed $line is empty or matches the beginning header - * pattern for a uuencoded message. - * - * @param string $line - * @return bool - */ - private function isEmptyOrStartLine($line) - { - return ($line === '' || preg_match('/^begin \d{3} .*$/', $line)); - } - - /** - * Returns true if the passed $line is either a backtick character '`' or - * the string 'end' signifying the end of the uuencoded message. - * - * @param string $line - * @return bool - */ - private function isEndLine($line) - { - return ($line === '`' || $line === 'end'); - } - - /** - * Filters a single line of encoded input. Returns NULL if the end has been - * reached. - * - * @param string $line - * @return string the decoded line - */ - private function filterLine($line) - { - $cur = ltrim(rtrim($line, "\t\n\r\0\x0B")); - if ($this->isEmptyOrStartLine($cur)) { - return ''; - } elseif ($this->isEndLine($cur)) { - return null; - } - return convert_uudecode($cur); - } - - /** - * Filters the lines in the passed $lines array, returning a concatenated - * string of decoded lines. - * - * @param array $lines - * @param int $consumed - * @return string - */ - private function filterBucketLines(array $lines, &$consumed) - { - $data = ''; - foreach ($lines as $line) { - $consumed += strlen($line); - $filtered = $this->filterLine($line); - if ($filtered === null) { - break; - } - $data .= $filtered; - } - return $data; - } - - /** - * Filter implementation converts encoding before returning PSFS_PASS_ON. - * - * @param resource $in - * @param resource $out - * @param int $consumed - * @param bool $closing - * @return int - */ - public function filter($in, $out, &$consumed, $closing) - { - while ($bucket = stream_bucket_make_writeable($in)) { - $lines = $this->getLines($bucket); - $converted = $this->filterBucketLines($lines, $consumed); - - // $this->stream is undocumented. It was found looking at HHVM's source code - // for its convert.iconv.* implementation in ConvertIconFilter and explained - // somewhat in this StackOverflow page: http://stackoverflow.com/a/31132646/335059 - // declaring a member variable called 'stream' breaks the PHP implementation (5.5.9 - // at least). - stream_bucket_append($out, stream_bucket_new($this->stream, $converted)); - } - return PSFS_PASS_ON; - } -} diff --git a/src/Stream/UUEncodeStreamFilter.php b/src/Stream/UUEncodeStreamFilter.php deleted file mode 100644 index 79cdca3a..00000000 --- a/src/Stream/UUEncodeStreamFilter.php +++ /dev/null @@ -1,134 +0,0 @@ -stream, $cleaned)); - } - - /** - * Writes out the header for a uuencoded part to the passed stream resource - * handle. - * - * @param resource $out - */ - private function writeUUEncodingHeader($out) - { - $data = 'begin 666 '; - if (isset($this->params['filename'])) { - $data .= $this->params['filename']; - } else { - $data .= 'null'; - } - stream_bucket_append($out, stream_bucket_new($this->stream, $data)); - } - - /** - * Returns the footer for a uuencoded part. - * - * @return string - */ - private function getUUEncodingFooter() - { - return "\r\n`\r\nend"; - } - - /** - * Reads from the input bucket stream, converts, and writes the uuencoded - * stream to $out. - * - * @param resource $in input bucket stream - * @param resource $out output bucket stream - * @param int $consumed incremented by number of bytes read from $in - */ - private function readAndConvert($in, $out, &$consumed) - { - while ($bucket = stream_bucket_make_writeable($in)) { - $data = $this->leftovers->value . $bucket->data; - if (!$this->headerWritten) { - $this->writeUUEncodingHeader($out); - $this->headerWritten = true; - } - $consumed += $bucket->datalen; - $nRemain = strlen($data) % 45; - $toConvert = $data; - if ($nRemain === 0) { - $this->leftovers->value = ''; - $this->leftovers->encodedValue = $this->getUUEncodingFooter(); - } else { - $this->leftovers->value = substr($data, -$nRemain); - $this->leftovers->encodedValue = "\r\n" . - rtrim(substr(rtrim(convert_uuencode($this->leftovers->value)), 0, -1)) - . $this->getUUEncodingFooter(); - $toConvert = substr($data, 0, -$nRemain); - } - $this->convertAndAppend($toConvert, $out); - } - } - - /** - * Filter implementation converts encoding before returning PSFS_PASS_ON. - * - * @param resource $in - * @param resource $out - * @param int $consumed - * @param bool $closing - * @return int - */ - public function filter($in, $out, &$consumed, $closing) - { - $this->readAndConvert($in, $out, $consumed); - return PSFS_PASS_ON; - } - - /** - * Sets up the leftovers object - */ - public function onCreate() - { - if (isset($this->params['leftovers'])) { - $this->leftovers = $this->params['leftovers']; - } - } -} diff --git a/tests/MailMimeParser/Header/AddressHeaderTest.php b/tests/MailMimeParser/Header/AddressHeaderTest.php index f557765f..b71b16b2 100644 --- a/tests/MailMimeParser/Header/AddressHeaderTest.php +++ b/tests/MailMimeParser/Header/AddressHeaderTest.php @@ -3,8 +3,6 @@ use PHPUnit_Framework_TestCase; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of AddressHeaderTest @@ -21,9 +19,10 @@ class AddressHeaderTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $this->consumerService = new ConsumerService($pf, $mlpf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $this->consumerService = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); } public function testEmptyHeader() diff --git a/tests/MailMimeParser/Header/Consumer/AddressBaseConsumerTest.php b/tests/MailMimeParser/Header/Consumer/AddressBaseConsumerTest.php index 63e23393..54b1343d 100644 --- a/tests/MailMimeParser/Header/Consumer/AddressBaseConsumerTest.php +++ b/tests/MailMimeParser/Header/Consumer/AddressBaseConsumerTest.php @@ -2,8 +2,6 @@ namespace ZBateson\MailMimeParser\Header\Consumer; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of AddressBaseConsumerTest @@ -20,10 +18,11 @@ class AddressBaseConsumerTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $cs = new ConsumerService($pf, $mlpf); - $this->addressBaseConsumer = AddressBaseConsumer::getInstance($cs, $pf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $cs = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); + $this->addressBaseConsumer = new AddressBaseConsumer($cs, $pf); } public function testConsumeAddress() diff --git a/tests/MailMimeParser/Header/Consumer/AddressConsumerTest.php b/tests/MailMimeParser/Header/Consumer/AddressConsumerTest.php index 952ed71c..6bf06645 100644 --- a/tests/MailMimeParser/Header/Consumer/AddressConsumerTest.php +++ b/tests/MailMimeParser/Header/Consumer/AddressConsumerTest.php @@ -2,8 +2,6 @@ namespace ZBateson\MailMimeParser\Header\Consumer; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of AddressEmailConsumerTest @@ -20,10 +18,11 @@ class AddressConsumerTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $cs = new ConsumerService($pf, $mlpf); - $this->addressConsumer = AddressConsumer::getInstance($cs, $pf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $cs = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); + $this->addressConsumer = new AddressConsumer($cs, $pf); } public function testConsumeEmail() diff --git a/tests/MailMimeParser/Header/Consumer/AddressGroupConsumerTest.php b/tests/MailMimeParser/Header/Consumer/AddressGroupConsumerTest.php index 33602a4f..bca039fb 100644 --- a/tests/MailMimeParser/Header/Consumer/AddressGroupConsumerTest.php +++ b/tests/MailMimeParser/Header/Consumer/AddressGroupConsumerTest.php @@ -2,8 +2,6 @@ namespace ZBateson\MailMimeParser\Header\Consumer; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of AddressGroupConsumerTest @@ -20,10 +18,11 @@ class AddressGroupConsumerTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $cs = new ConsumerService($pf, $mlpf); - $this->addressGroupConsumer = AddressGroupConsumer::getInstance($cs, $pf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $cs = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); + $this->addressGroupConsumer = new AddressGroupConsumer($cs, $pf); } public function testConsumeGroup() diff --git a/tests/MailMimeParser/Header/Consumer/CommentConsumerTest.php b/tests/MailMimeParser/Header/Consumer/CommentConsumerTest.php index 0bcc3c85..4671e5dd 100644 --- a/tests/MailMimeParser/Header/Consumer/CommentConsumerTest.php +++ b/tests/MailMimeParser/Header/Consumer/CommentConsumerTest.php @@ -2,8 +2,6 @@ namespace ZBateson\MailMimeParser\Header\Consumer; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of CommentConsumerTest @@ -20,10 +18,11 @@ class CommentConsumerTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $cs = new ConsumerService($pf, $mlpf); - $this->commentConsumer = CommentConsumer::getInstance($cs, $pf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $cs = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); + $this->commentConsumer = new CommentConsumer($cs, $pf); } protected function assertCommentConsumed($expected, $value) diff --git a/tests/MailMimeParser/Header/Consumer/ConsumerServiceTest.php b/tests/MailMimeParser/Header/Consumer/ConsumerServiceTest.php index ef97d445..9e04a29b 100644 --- a/tests/MailMimeParser/Header/Consumer/ConsumerServiceTest.php +++ b/tests/MailMimeParser/Header/Consumer/ConsumerServiceTest.php @@ -2,8 +2,6 @@ namespace ZBateson\MailMimeParser\Header\Consumer; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of ConsumerServiceTest @@ -20,8 +18,9 @@ class ConsumerServiceTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); $this->consumerService = new ConsumerService($pf, $mlpf); } diff --git a/tests/MailMimeParser/Header/Consumer/DateConsumerTest.php b/tests/MailMimeParser/Header/Consumer/DateConsumerTest.php index eb2fb418..08a1a881 100644 --- a/tests/MailMimeParser/Header/Consumer/DateConsumerTest.php +++ b/tests/MailMimeParser/Header/Consumer/DateConsumerTest.php @@ -3,8 +3,6 @@ use PHPUnit_Framework_TestCase; use DateTime; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of DateConsumerTest @@ -21,10 +19,11 @@ class DateConsumerTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $cs = new ConsumerService($pf, $mlpf); - $this->dateConsumer = DateConsumer::getInstance($cs, $pf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $cs = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); + $this->dateConsumer = new DateConsumer($cs, $pf); } public function testConsumeDates() diff --git a/tests/MailMimeParser/Header/Consumer/GenericConsumerTest.php b/tests/MailMimeParser/Header/Consumer/GenericConsumerTest.php index a05b7d8e..ade7b639 100644 --- a/tests/MailMimeParser/Header/Consumer/GenericConsumerTest.php +++ b/tests/MailMimeParser/Header/Consumer/GenericConsumerTest.php @@ -2,8 +2,6 @@ namespace ZBateson\MailMimeParser\Header\Consumer; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of GenericConsumerTest @@ -20,10 +18,11 @@ class GenericConsumerTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $cs = new ConsumerService($pf, $mlpf); - $this->genericConsumer = GenericConsumer::getInstance($cs, $mlpf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $cs = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); + $this->genericConsumer = new GenericConsumer($cs, $mlpf); } public function testConsumeTokens() diff --git a/tests/MailMimeParser/Header/Consumer/ParameterConsumerTest.php b/tests/MailMimeParser/Header/Consumer/ParameterConsumerTest.php index 833b219e..c3437139 100644 --- a/tests/MailMimeParser/Header/Consumer/ParameterConsumerTest.php +++ b/tests/MailMimeParser/Header/Consumer/ParameterConsumerTest.php @@ -2,8 +2,6 @@ namespace ZBateson\MailMimeParser\Header\Consumer; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of ParameterConsumerTest @@ -20,10 +18,11 @@ class ParameterConsumerTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $cs = new ConsumerService($pf, $mlpf); - $this->parameterConsumer = ParameterConsumer::getInstance($cs, $pf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $cs = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); + $this->parameterConsumer = new ParameterConsumer($cs, $pf); } public function testConsumeTokens() @@ -59,4 +58,82 @@ public function testWithSubConsumers() $this->assertEquals('toppings', $ret[2]->getName()); $this->assertEquals('sriracha', $ret[2]->getValue()); } + + public function testSimpleSplitHeader() + { + $ret = $this->parameterConsumer->__invoke('hotdogs; condiments*0="mustar";' + . 'condiments*1="d, ketchup"; condiments*2=" and mayo"'); + $this->assertNotEmpty($ret); + $this->assertCount(2, $ret); + $this->assertEquals('hotdogs', $ret[0]->getValue()); + $this->assertEquals('condiments', $ret[1]->getName()); + $this->assertEquals('mustard, ketchup and mayo', $ret[1]->getValue()); + $this->assertNull($ret[1]->getLanguage()); + } + + public function testSplitHeaderInFunnyOrder() + { + $ret = $this->parameterConsumer->__invoke('hotdogs; condiments*2=" and mayo";' + . 'condiments*1="d, ketchup"; condiments*0="mustar"'); + $this->assertNotEmpty($ret); + $this->assertCount(2, $ret); + $this->assertEquals('hotdogs', $ret[0]->getValue()); + $this->assertEquals('condiments', $ret[1]->getName()); + $this->assertEquals('mustard, ketchup and mayo', $ret[1]->getValue()); + $this->assertNull($ret[1]->getLanguage()); + } + + public function testSplitHeaderWithEmptyEncodingAndLanguage() + { + $ret = $this->parameterConsumer->__invoke('hotdogs; condiments*=\'\'' + . 'mustard,%20ketchup%20and%20mayo'); + $this->assertNotEmpty($ret); + $this->assertCount(2, $ret); + $this->assertEquals('hotdogs', $ret[0]->getValue()); + $this->assertEquals('condiments', $ret[1]->getName()); + $this->assertEquals('mustard, ketchup and mayo', $ret[1]->getValue()); + $this->assertNull($ret[1]->getLanguage()); + } + + public function testSplitHeaderWithEncodingAndLanguage() + { + $ret = $this->parameterConsumer->__invoke('hotdogs; condiments*=us-ascii\'en-US\'' + . 'mustard,%20ketchup%20and%20mayo'); + $this->assertNotEmpty($ret); + $this->assertCount(2, $ret); + $this->assertEquals('hotdogs', $ret[0]->getValue()); + $this->assertEquals('condiments', $ret[1]->getName()); + $this->assertEquals('mustard, ketchup and mayo', $ret[1]->getValue()); + $this->assertEquals('en-US', $ret[1]->getLanguage()); + } + + public function testSplitHeaderWithMultiByteEncodedPart() + { + $ret = $this->parameterConsumer->__invoke('hotdogs; condiments*=utf-8\'\'' + . 'mustardized%E2%80%93ketchup'); + $this->assertNotEmpty($ret); + $this->assertCount(2, $ret); + $this->assertEquals('hotdogs', $ret[0]->getValue()); + $this->assertEquals('condiments', $ret[1]->getName()); + $this->assertEquals('mustardized–ketchup', $ret[1]->getValue()); + $this->assertNull($ret[1]->getLanguage()); + } + + public function testSplitHeaderWithMultiByteEncodedPartAndLanguage() + { + $str = 'هلا هلا شخبار بعد؟ شلون تبرمج؟'; + $encoded = rawurlencode($str); + $halfPos = floor((strlen($encoded) / 3) / 2) * 3; + $part1 = substr($encoded, 0, $halfPos); + $part2 = substr($encoded, $halfPos); + + $ret = $this->parameterConsumer->__invoke('hotdogs; condiments*0*=utf-8\'abv-BH\''. $part1 + . '; condiments*1*=' . $part2); + $this->assertNotEmpty($ret); + $this->assertCount(2, $ret); + $this->assertEquals('hotdogs', $ret[0]->getValue()); + $this->assertEquals('condiments', $ret[1]->getName()); + $this->assertEquals($str, $ret[1]->getValue()); + $this->assertEquals('abv-BH', $ret[1]->getLanguage()); + } } diff --git a/tests/MailMimeParser/Header/Consumer/QuotedStringConsumerTest.php b/tests/MailMimeParser/Header/Consumer/QuotedStringConsumerTest.php index 87bc58ce..87a550b1 100644 --- a/tests/MailMimeParser/Header/Consumer/QuotedStringConsumerTest.php +++ b/tests/MailMimeParser/Header/Consumer/QuotedStringConsumerTest.php @@ -2,8 +2,6 @@ namespace ZBateson\MailMimeParser\Header\Consumer; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of QuotedStringConsumerTest @@ -20,10 +18,11 @@ class QuotedStringConsumerTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $cs = new ConsumerService($pf, $mlpf); - $this->quotedStringConsumer = QuotedStringConsumer::getInstance($cs, $pf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $cs = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); + $this->quotedStringConsumer = new QuotedStringConsumer($cs, $pf); } public function testConsumeTokens() diff --git a/tests/MailMimeParser/Header/Consumer/SubjectConsumerTest.php b/tests/MailMimeParser/Header/Consumer/SubjectConsumerTest.php index fa8de190..2ce86a31 100644 --- a/tests/MailMimeParser/Header/Consumer/SubjectConsumerTest.php +++ b/tests/MailMimeParser/Header/Consumer/SubjectConsumerTest.php @@ -2,8 +2,6 @@ namespace ZBateson\MailMimeParser\Header\Consumer; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of SubjectConsumerTest @@ -20,10 +18,11 @@ class SubjectConsumerTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $cs = new ConsumerService($pf, $mlpf); - $this->subjectConsumer = SubjectConsumer::getInstance($cs, $mlpf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $cs = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); + $this->subjectConsumer = new SubjectConsumer($cs, $mlpf); } public function testConsumeTokens() diff --git a/tests/MailMimeParser/Header/DateHeaderTest.php b/tests/MailMimeParser/Header/DateHeaderTest.php index 1c998f48..4eeb6c53 100644 --- a/tests/MailMimeParser/Header/DateHeaderTest.php +++ b/tests/MailMimeParser/Header/DateHeaderTest.php @@ -2,9 +2,6 @@ namespace ZBateson\MailMimeParser\Header; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of DateHeaderTest @@ -21,9 +18,10 @@ class DateHeaderTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $this->consumerService = new ConsumerService($pf, $mlpf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $this->consumerService = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); } public function testSimpleDate() diff --git a/tests/MailMimeParser/Header/GenericHeaderTest.php b/tests/MailMimeParser/Header/GenericHeaderTest.php index d0011904..94bd4266 100644 --- a/tests/MailMimeParser/Header/GenericHeaderTest.php +++ b/tests/MailMimeParser/Header/GenericHeaderTest.php @@ -2,9 +2,6 @@ namespace ZBateson\MailMimeParser\Header; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of GenericHeaderTest @@ -21,9 +18,10 @@ class GenericHeaderTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $this->consumerService = new ConsumerService($pf, $mlpf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $this->consumerService = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); } public function testParsing() diff --git a/tests/MailMimeParser/Header/HeaderFactoryTest.php b/tests/MailMimeParser/Header/HeaderFactoryTest.php index 9b5fc2ec..8556ff99 100644 --- a/tests/MailMimeParser/Header/HeaderFactoryTest.php +++ b/tests/MailMimeParser/Header/HeaderFactoryTest.php @@ -2,9 +2,6 @@ namespace ZBateson\MailMimeParser\Header; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of HeaderFactoryTest @@ -20,9 +17,10 @@ class HeaderFactoryTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $cs = new ConsumerService($pf, $mlpf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $cs = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); $this->headerFactory = new HeaderFactory($cs, $pf); } diff --git a/tests/MailMimeParser/Header/ParameterHeaderTest.php b/tests/MailMimeParser/Header/ParameterHeaderTest.php index 84c1c9e6..af4312c1 100644 --- a/tests/MailMimeParser/Header/ParameterHeaderTest.php +++ b/tests/MailMimeParser/Header/ParameterHeaderTest.php @@ -2,9 +2,6 @@ namespace ZBateson\MailMimeParser\Header; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of ParametersHeaderTest @@ -21,9 +18,16 @@ class ParameterHeaderTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $this->consumerService = new ConsumerService($pf, $mlpf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $this->consumerService = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); + } + + public function testParsingContentTypeWithoutParameters() + { + $header = new ParameterHeader($this->consumerService, 'Content-Type', 'text/html'); + $this->assertEquals('text/html', $header->getValue()); } public function testParsingContentType() diff --git a/tests/MailMimeParser/Header/Part/AddressGroupPartTest.php b/tests/MailMimeParser/Header/Part/AddressGroupPartTest.php index 452e4de2..6dc475c2 100644 --- a/tests/MailMimeParser/Header/Part/AddressGroupPartTest.php +++ b/tests/MailMimeParser/Header/Part/AddressGroupPartTest.php @@ -18,7 +18,8 @@ public function testNameGroup() { $name = 'Roman Senate'; $members = ['Caesar', 'Cicero', 'Cato']; - $part = new AddressGroupPart($members, $name); + $csConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); + $part = new AddressGroupPart($csConverter, $members, $name); $this->assertEquals($name, $part->getName()); $this->assertEquals($members, $part->getAddresses()); $this->assertEquals($members[0], $part->getAddress(0)); diff --git a/tests/MailMimeParser/Header/Part/AddressPartTest.php b/tests/MailMimeParser/Header/Part/AddressPartTest.php index de83b716..a0c860fc 100644 --- a/tests/MailMimeParser/Header/Part/AddressPartTest.php +++ b/tests/MailMimeParser/Header/Part/AddressPartTest.php @@ -14,11 +14,18 @@ */ class AddressPartTest extends PHPUnit_Framework_TestCase { + private $charsetConverter; + + public function setUp() + { + $this->charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); + } + public function testNameEmail() { $name = 'Julius Caeser'; $email = 'gaius@altavista.com'; - $part = new AddressPart($name, $email); + $part = new AddressPart($this->charsetConverter, $name, $email); $this->assertEquals($name, $part->getName()); $this->assertEquals($email, $part->getEmail()); } @@ -26,7 +33,7 @@ public function testNameEmail() public function testEmailSpacesStripped() { $email = "gaius julius\t\n caesar@altavista.com"; - $part = new AddressPart('', $email); + $part = new AddressPart($this->charsetConverter, '', $email); $this->assertEquals('gaiusjuliuscaesar@altavista.com', $part->getEmail()); } } diff --git a/tests/MailMimeParser/Header/Part/CommentPartTest.php b/tests/MailMimeParser/Header/Part/CommentPartTest.php index e0009b31..d9be341f 100644 --- a/tests/MailMimeParser/Header/Part/CommentPartTest.php +++ b/tests/MailMimeParser/Header/Part/CommentPartTest.php @@ -14,17 +14,28 @@ */ class CommentPartTest extends PHPUnit_Framework_TestCase { + private $charsetConverter; + + public function setUp() + { + $this->charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); + } + public function testBasicComment() { $comment = 'Some silly comment made about my moustache'; - $part = new CommentPart($comment); + $part = new CommentPart($this->charsetConverter, $comment); $this->assertEquals('', $part->getValue()); $this->assertEquals($comment, $part->getComment()); } public function testMimeEncoding() { - $part = new CommentPart('=?US-ASCII?Q?Kilgore_Trout?='); + $this->charsetConverter->expects($this->once()) + ->method('convert') + ->with('Kilgore Trout', 'US-ASCII', 'UTF-8') + ->willReturn('Kilgore Trout'); + $part = new CommentPart($this->charsetConverter, '=?US-ASCII?Q?Kilgore_Trout?='); $this->assertEquals('', $part->getValue()); $this->assertEquals('Kilgore Trout', $part->getComment()); } diff --git a/tests/MailMimeParser/Header/Part/DatePartTest.php b/tests/MailMimeParser/Header/Part/DatePartTest.php index 306f0f88..720bb678 100644 --- a/tests/MailMimeParser/Header/Part/DatePartTest.php +++ b/tests/MailMimeParser/Header/Part/DatePartTest.php @@ -15,10 +15,17 @@ */ class DatePartTest extends PHPUnit_Framework_TestCase { + private $charsetConverter; + + public function setUp() + { + $this->charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); + } + public function testDateString() { $value = 'Wed, 17 May 2000 19:08:29 -0400'; - $part = new DatePart($value); + $part = new DatePart($this->charsetConverter, $value); $this->assertEquals($value, $part->getValue()); $date = $part->getDateTime(); $this->assertNotEmpty($date); @@ -28,7 +35,7 @@ public function testDateString() public function testInvalidDate() { $value = 'Invalid Date'; - $part = new DatePart($value); + $part = new DatePart($this->charsetConverter, $value); $this->assertEquals($value, $part->getValue()); $date = $part->getDateTime(); $this->assertNull($date); diff --git a/tests/MailMimeParser/Header/Part/HeaderPartFactoryTest.php b/tests/MailMimeParser/Header/Part/HeaderPartFactoryTest.php index d40bd33b..7951682b 100644 --- a/tests/MailMimeParser/Header/Part/HeaderPartFactoryTest.php +++ b/tests/MailMimeParser/Header/Part/HeaderPartFactoryTest.php @@ -13,11 +13,12 @@ */ class HeaderPartFactoryTest extends PHPUnit_Framework_TestCase { - protected $headerPartFactory; + private $headerPartFactory; protected function setUp() { - $this->headerPartFactory = new HeaderPartFactory(); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); + $this->headerPartFactory = new HeaderPartFactory($charsetConverter); } public function testNewInstance() @@ -34,6 +35,13 @@ public function testNewToken() $this->assertInstanceOf('\ZBateson\MailMimeParser\Header\Part\Token', $token); } + public function testNewSplitParameterToken() + { + $token = $this->headerPartFactory->newSplitParameterToken('Test'); + $this->assertNotNull($token); + $this->assertInstanceOf('\ZBateson\MailMimeParser\Header\Part\SplitParameterToken', $token); + } + public function testNewLiteralPart() { $part = $this->headerPartFactory->newLiteralPart('Test'); diff --git a/tests/MailMimeParser/Header/Part/HeaderPartTest.php b/tests/MailMimeParser/Header/Part/HeaderPartTest.php index 5a94b8a2..1d506404 100644 --- a/tests/MailMimeParser/Header/Part/HeaderPartTest.php +++ b/tests/MailMimeParser/Header/Part/HeaderPartTest.php @@ -17,7 +17,9 @@ class HeaderPartTest extends PHPUnit_Framework_TestCase protected function setUp() { + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); $stub = $this->getMockBuilder('\ZBateson\MailMimeParser\Header\Part\HeaderPart') + ->setConstructorArgs([$charsetConverter]) ->getMockForAbstractClass(); $this->abstractHeaderPartStub = $stub; } diff --git a/tests/MailMimeParser/Header/Part/LiteralPartTest.php b/tests/MailMimeParser/Header/Part/LiteralPartTest.php index a2316c28..a447064d 100644 --- a/tests/MailMimeParser/Header/Part/LiteralPartTest.php +++ b/tests/MailMimeParser/Header/Part/LiteralPartTest.php @@ -16,11 +16,13 @@ class LiteralPartTest extends PHPUnit_Framework_TestCase { public function testInstance() { - $part = new LiteralPart('"'); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); + + $part = new LiteralPart($charsetConverter, '"'); $this->assertNotNull($part); $this->assertEquals('"', $part->getValue()); - $part = new LiteralPart('=?US-ASCII?Q?Kilgore_Trout?='); + $part = new LiteralPart($charsetConverter, '=?US-ASCII?Q?Kilgore_Trout?='); $this->assertEquals('=?US-ASCII?Q?Kilgore_Trout?=', $part->getValue()); } } diff --git a/tests/MailMimeParser/Header/Part/MimeLiteralPartFactoryTest.php b/tests/MailMimeParser/Header/Part/MimeLiteralPartFactoryTest.php index c0a1a4c2..2b410327 100644 --- a/tests/MailMimeParser/Header/Part/MimeLiteralPartFactoryTest.php +++ b/tests/MailMimeParser/Header/Part/MimeLiteralPartFactoryTest.php @@ -17,7 +17,8 @@ class MimeLiteralPartFactoryTest extends PHPUnit_Framework_TestCase protected function setUp() { - $this->headerPartFactory = new MimeLiteralPartFactory(); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); + $this->headerPartFactory = new MimeLiteralPartFactory($charsetConverter); } public function testNewInstance() diff --git a/tests/MailMimeParser/Header/Part/MimeLiteralPartTest.php b/tests/MailMimeParser/Header/Part/MimeLiteralPartTest.php index 1fe86158..e122ba0a 100644 --- a/tests/MailMimeParser/Header/Part/MimeLiteralPartTest.php +++ b/tests/MailMimeParser/Header/Part/MimeLiteralPartTest.php @@ -14,27 +14,84 @@ */ class MimeLiteralPartTest extends PHPUnit_Framework_TestCase { + private $charsetConverter; + + public function setUp() + { + $this->charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); + } + protected function assertDecoded($expected, $encodedActual) { - $part = new MimeLiteralPart($encodedActual); + $part = new MimeLiteralPart($this->charsetConverter, $encodedActual); $this->assertEquals($expected, $part->getValue()); + return $part; } public function testBasicValue() { + $this->charsetConverter->expects($this->never()) + ->method('convert'); $this->assertDecoded('Step', 'Step'); } + public function testNullLanguage() + { + $this->charsetConverter->expects($this->never()) + ->method('convert'); + $part = $this->assertDecoded('Step', 'Step'); + $this->assertEquals([ + [ 'lang' => null, 'value' => 'Step' ] + ], $part->getLanguageArray()); + } + public function testMimeEncoding() { + $this->charsetConverter->expects($this->once()) + ->method('convert') + ->with('Kilgore Trout', 'US-ASCII', 'UTF-8') + ->willReturn('Kilgore Trout'); $this->assertDecoded('Kilgore Trout', '=?US-ASCII?Q?Kilgore_Trout?='); } + public function testMimeEncodingNullLanguage() + { + $this->charsetConverter->expects($this->once()) + ->method('convert') + ->with('Kilgore Trout', 'US-ASCII', 'UTF-8') + ->willReturn('Kilgore Trout'); + $part = $this->assertDecoded('Kilgore Trout', '=?US-ASCII?Q?Kilgore_Trout?='); + $this->assertEquals([ + [ 'lang' => null, 'value' => 'Kilgore Trout' ] + ], $part->getLanguageArray()); + } + public function testEncodingTwoParts() { $kilgore = '=?US-ASCII?Q?Kilgore_Trout?='; $snow = '=?US-ASCII?Q?Jon_Snow?='; + $this->charsetConverter->expects($this->exactly(7)) + ->method('convert') + ->withConsecutive( + ['Kilgore Trout', 'US-ASCII', 'UTF-8'], + ['Jon Snow', 'US-ASCII', 'UTF-8'], + ['Kilgore Trout', 'US-ASCII', 'UTF-8'], + ['Jon Snow', 'US-ASCII', 'UTF-8'], + ['Kilgore Trout', 'US-ASCII', 'UTF-8'], + ['Jon Snow', 'US-ASCII', 'UTF-8'], + ['Jon Snow', 'US-ASCII', 'UTF-8'] + ) + ->willReturnOnConsecutiveCalls( + 'Kilgore Trout', + 'Jon Snow', + 'Kilgore Trout', + 'Jon Snow', + 'Kilgore Trout', + 'Jon Snow', + 'Jon Snow' + ); + $this->assertDecoded( ' Kilgore TroutJon Snow ', " $kilgore $snow " @@ -59,6 +116,10 @@ public function testEncodingTwoParts() public function testNonAscii() { + $this->charsetConverter = $this->getMockBuilder('ZBateson\StreamDecorators\Util\CharsetConverter') + ->setMethods(['__toString']) + ->getMock(); + $this->assertDecoded( 'κόσμε fløde', '=?UTF-8?B?zrrhvbnPg868zrUgZmzDuGRl?=' @@ -105,22 +166,41 @@ public function testNonAscii() public function testIgnoreSpacesBefore() { - $part = new MimeLiteralPart('=?US-ASCII?Q?Kilgore_Trout?=Blah'); + $part = new MimeLiteralPart($this->charsetConverter, '=?US-ASCII?Q?Kilgore_Trout?=Blah'); $this->assertTrue($part->ignoreSpacesBefore(), 'ignore spaces before'); $this->assertFalse($part->ignoreSpacesAfter(), 'ignore spaces after'); } public function testIgnoreSpacesAfter() { - $part = new MimeLiteralPart('Blah=?US-ASCII?Q?Kilgore_Trout?='); + $part = new MimeLiteralPart($this->charsetConverter, 'Blah=?US-ASCII?Q?Kilgore_Trout?='); $this->assertFalse($part->ignoreSpacesBefore(), 'ignore spaces before'); $this->assertTrue($part->ignoreSpacesAfter(), 'ignore spaces after'); } public function testIgnoreSpacesBeforeAndAfter() { - $part = new MimeLiteralPart('=?US-ASCII?Q?Kilgore_Trout?='); + $part = new MimeLiteralPart($this->charsetConverter, '=?US-ASCII?Q?Kilgore_Trout?='); $this->assertTrue($part->ignoreSpacesBefore(), 'ignore spaces before'); $this->assertTrue($part->ignoreSpacesAfter(), 'ignore spaces after'); } + + public function testLanguageParts() + { + $this->charsetConverter = $this->getMockBuilder('ZBateson\StreamDecorators\Util\CharsetConverter') + ->setMethods(['__toString']) + ->getMock(); + + $part = $this->assertDecoded( + 'Hello and bonjour mi amici. Welcome!', + 'Hello and =?UTF-8*fr-be?Q?bonjour_?= =?UTF-8*it?Q?mi amici?=. Welcome!' + ); + $expectedLang = [ + [ 'lang' => null, 'value' => 'Hello and ' ], + [ 'lang' => 'fr-be', 'value' => 'bonjour ' ], + [ 'lang' => 'it', 'value' => 'mi amici' ], + [ 'lang' => null, 'value' => '. Welcome!' ] + ]; + $this->assertEquals($expectedLang, $part->getLanguageArray()); + } } diff --git a/tests/MailMimeParser/Header/Part/ParameterPartTest.php b/tests/MailMimeParser/Header/Part/ParameterPartTest.php index d3533236..aa9fcba4 100644 --- a/tests/MailMimeParser/Header/Part/ParameterPartTest.php +++ b/tests/MailMimeParser/Header/Part/ParameterPartTest.php @@ -14,24 +14,56 @@ */ class ParameterPartTest extends PHPUnit_Framework_TestCase { + private $charsetConverter; + + public function setUp() + { + $this->charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); + } + public function testBasicNameValuePair() { - $part = new ParameterPart('Name', 'Value'); + $part = new ParameterPart($this->charsetConverter, 'Name', 'Value'); $this->assertEquals('Name', $part->getName()); $this->assertEquals('Value', $part->getValue()); } public function testMimeValue() { - $part = new ParameterPart('name', '=?US-ASCII?Q?Kilgore_Trout?='); + $this->charsetConverter->expects($this->once()) + ->method('convert') + ->with('Kilgore Trout', 'US-ASCII', 'UTF-8') + ->willReturn('Kilgore Trout'); + $part = new ParameterPart($this->charsetConverter, 'name', '=?US-ASCII?Q?Kilgore_Trout?='); $this->assertEquals('name', $part->getName()); $this->assertEquals('Kilgore Trout', $part->getValue()); } public function testMimeName() { - $part = new ParameterPart('=?US-ASCII?Q?name?=', 'Kilgore'); + $this->charsetConverter->expects($this->once()) + ->method('convert') + ->with('name', 'US-ASCII', 'UTF-8') + ->willReturn('name'); + $part = new ParameterPart($this->charsetConverter, '=?US-ASCII?Q?name?=', 'Kilgore'); $this->assertEquals('name', $part->getName()); $this->assertEquals('Kilgore', $part->getValue()); } + + public function testNameValueNotDecodedWithLanguage() + { + $this->charsetConverter->expects($this->never()) + ->method('convert'); + $part = new ParameterPart($this->charsetConverter, '=?US-ASCII?Q?name?=', '=?US-ASCII?Q?Kilgore_Trout?=', 'Kurty'); + $this->assertEquals('=?US-ASCII?Q?name?=', $part->getName()); + $this->assertEquals('=?US-ASCII?Q?Kilgore_Trout?=', $part->getValue()); + } + + public function testGetLanguage() + { + $this->charsetConverter->expects($this->never()) + ->method('convert'); + $part = new ParameterPart($this->charsetConverter, 'name', 'Drogo', 'Dothraki'); + $this->assertEquals('Dothraki', $part->getLanguage()); + } } diff --git a/tests/MailMimeParser/Header/Part/SplitParameterTokenTest.php b/tests/MailMimeParser/Header/Part/SplitParameterTokenTest.php new file mode 100644 index 00000000..67621591 --- /dev/null +++ b/tests/MailMimeParser/Header/Part/SplitParameterTokenTest.php @@ -0,0 +1,187 @@ +charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); + } + + public function testGetNameAndNullLanguage() + { + $part = new SplitParameterToken($this->charsetConverter, ' Drogo '); + $this->assertEquals('Drogo', $part->getName()); + $this->assertNull($part->getLanguage()); + } + + public function testLanguageIsSetBeforeGetValue() + { + $part = new SplitParameterToken($this->charsetConverter, ' Drogo '); + $part->addPart('unknown\'Dothraki\'blah', true, ''); + $this->assertEquals('Dothraki', $part->getLanguage()); + } + + public function testLanguageIsNullForEmptyEncodedLanguage() + { + $part = new SplitParameterToken($this->charsetConverter, 'name'); + $part->addPart('unknown\'\'Khal%20Drogo,%20', true, 0); + $this->assertNull($part->getLanguage()); + } + + public function testAddLiteralPart() + { + $part = new SplitParameterToken($this->charsetConverter, 'name'); + $part->addPart('Khal Drogo', false, 0); + $this->assertEquals('Khal Drogo', $part->getValue()); + } + + public function testAddMultipleLiteralParts() + { + $part = new SplitParameterToken($this->charsetConverter, 'name'); + $part->addPart('Khal ', false, 0); + $part->addPart('Drogo', false, 1); + $this->assertEquals('Khal Drogo', $part->getValue()); + } + + public function testAddUnsortedMultipleLiteralParts() + { + $part = new SplitParameterToken($this->charsetConverter, 'name'); + $part->addPart('Dro', false, 1); + $part->addPart('Khal ', false, 0); + $part->addPart('go', false, 2); + $this->assertEquals('Khal Drogo', $part->getValue()); + } + + public function testAddEncodedPart() + { + $this->charsetConverter->expects($this->once()) + ->method('convert') + ->with('Khal Drogo', 'ISO-8859-1', 'UTF-8') + ->willReturn('Khal Drogo'); + $part = new SplitParameterToken($this->charsetConverter, 'name'); + $part->addPart('Khal%20Drogo', true, 0); + $this->assertEquals('Khal Drogo', $part->getValue()); + } + + public function testAddMultiEncodedPart() + { + $this->charsetConverter->expects($this->once()) + ->method('convert') + ->with( + 'Khal Drogo, Ruler of his Khalisar', 'ISO-8859-1', 'UTF-8' + ) + ->willReturn('Khal Drogo, Ruler of his Khalisar'); + + $part = new SplitParameterToken($this->charsetConverter, 'name'); + $part->addPart('Khal%20Drogo,%20', true, 0); + $part->addPart('Ruler%20of%20', true, 1); + $part->addPart('his%20', true, 2); + $part->addPart('Khalisar', true, 3); + $this->assertEquals('Khal Drogo, Ruler of his Khalisar', $part->getValue()); + } + + public function testAddUnsortedMultiEncodedPart() + { + $this->charsetConverter->expects($this->once()) + ->method('convert') + ->with( + 'Khal Drogo, Ruler of his Khalisar', 'ISO-8859-1', 'UTF-8' + ) + ->willReturn('Khal Drogo, Ruler of his Khalisar'); + + $part = new SplitParameterToken($this->charsetConverter, 'name'); + $part->addPart('Khalisar', true, 3); + $part->addPart('Khal%20Drogo,%20', true, 0); + $part->addPart('his%20', true, 2); + $part->addPart('Ruler%20of%20', true, 1); + $this->assertEquals('Khal Drogo, Ruler of his Khalisar', $part->getValue()); + } + + public function testAddUnsortedMultiEncodedPartWithLanguage() + { + $this->charsetConverter->expects($this->once()) + ->method('convert') + ->with( + 'Khal Drogo, Ruler of his Khalisar', 'us-ascii', 'UTF-8' + ) + ->willReturn('Khal Drogo, Ruler of his Khalisar'); + + $part = new SplitParameterToken($this->charsetConverter, 'name'); + $part->addPart('Khalisar', true, 3); + $part->addPart('us-ascii\'dothraki-LHAZ\'Khal%20Drogo,%20', true, 0); + $part->addPart('his%20', true, 2); + $part->addPart('Ruler%20of%20', true, 1); + $this->assertEquals('Khal Drogo, Ruler of his Khalisar', $part->getValue()); + $this->assertEquals('dothraki-LHAZ', $part->getLanguage()); + } + + public function testLanguageNotSetOnNonZeroPart() + { + $this->charsetConverter->expects($this->once()) + ->method('convert') + ->with( + 'Khal Drogo, Ruler of his Khalisar', 'us-ascii', 'UTF-8' + ) + ->willReturn('Khal Drogo, Ruler of his Khalisar'); + + $part = new SplitParameterToken($this->charsetConverter, 'name'); + $part->addPart('Khalisar', true, 3); + $part->addPart('us-ascii\'dothraki-LHAZ\'Khal%20Drogo,%20', true, 0); + $part->addPart('his%20', true, 2); + $part->addPart('charset\'other-lang\'Ruler%20of%20', true, 1); + $this->assertEquals('Khal Drogo, Ruler of his Khalisar', $part->getValue()); + $this->assertEquals('dothraki-LHAZ', $part->getLanguage()); + } + + public function testAddMixedEncodedAndNonEncodedCombinesCharsetConversion() + { + $this->charsetConverter->expects($this->exactly(2)) + ->method('convert') + ->withConsecutive( + [ 'Khal Drogo, Ruler of ', 'us-ascii', 'UTF-8' ], + [ 'Khalisar', 'us-ascii', 'UTF-8' ] + ) + ->willReturnOnConsecutiveCalls('Khal Drogo, Ruler of ', 'Khalisar'); + + $part = new SplitParameterToken($this->charsetConverter, 'name'); + $part->addPart('us-ascii\'dothraki-LHAZ\'Khal%20Drogo,%20', true, 0); + $part->addPart('Ruler%20of%20', true, 1); + $part->addPart('his ', false, 2); + $part->addPart('Khalisar', true, 3); + + $this->assertEquals('Khal Drogo, Ruler of his Khalisar', $part->getValue()); + } + + public function testAddUnsortedMixedEncodedAndNonEncodedCombinesCharsetConversion() + { + $this->charsetConverter->expects($this->exactly(2)) + ->method('convert') + ->withConsecutive( + [ 'Khal Drogo, Ruler of ', 'us-ascii', 'UTF-8' ], + [ 'Khalisar', 'us-ascii', 'UTF-8' ] + ) + ->willReturnOnConsecutiveCalls('Khal Drogo, Ruler of ', 'Khalisar'); + + $part = new SplitParameterToken($this->charsetConverter, 'name'); + $part->addPart('Khalisar', true, 3); + $part->addPart('Ruler%20of%20', true, 1); + $part->addPart('us-ascii\'dothraki-LHAZ\'Khal%20Drogo,%20', true, 0); + $part->addPart('his ', false, 2); + + $this->assertEquals('Khal Drogo, Ruler of his Khalisar', $part->getValue()); + } +} diff --git a/tests/MailMimeParser/Header/Part/TokenTest.php b/tests/MailMimeParser/Header/Part/TokenTest.php index bed87f38..a229b773 100644 --- a/tests/MailMimeParser/Header/Part/TokenTest.php +++ b/tests/MailMimeParser/Header/Part/TokenTest.php @@ -14,9 +14,16 @@ */ class TokenTest extends PHPUnit_Framework_TestCase { + private $charsetConverter; + + public function setUp() + { + $this->charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter'); + } + public function testInstance() { - $token = new Token('testing'); + $token = new Token($this->charsetConverter, 'testing'); $this->assertNotNull($token); $this->assertEquals('testing', $token->getValue()); $this->assertEquals('testing', strval($token)); @@ -24,14 +31,14 @@ public function testInstance() public function testSpaceTokenValue() { - $token = new Token(' '); + $token = new Token($this->charsetConverter, ' '); $this->assertTrue($token->ignoreSpacesBefore()); $this->assertTrue($token->ignoreSpacesAfter()); } public function testNonSpaceTokenValue() { - $token = new Token('Anything'); + $token = new Token($this->charsetConverter, 'Anything'); $this->assertFalse($token->ignoreSpacesBefore()); $this->assertFalse($token->ignoreSpacesAfter()); } diff --git a/tests/MailMimeParser/Header/SubjectHeaderTest.php b/tests/MailMimeParser/Header/SubjectHeaderTest.php index e28bd99e..5a553dd0 100644 --- a/tests/MailMimeParser/Header/SubjectHeaderTest.php +++ b/tests/MailMimeParser/Header/SubjectHeaderTest.php @@ -2,9 +2,6 @@ namespace ZBateson\MailMimeParser\Header; use PHPUnit_Framework_TestCase; -use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; -use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; -use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Description of SubjectHeader @@ -21,9 +18,10 @@ class SubjectHeaderTest extends PHPUnit_Framework_TestCase protected function setUp() { - $pf = new HeaderPartFactory(); - $mlpf = new MimeLiteralPartFactory(); - $this->consumerService = new ConsumerService($pf, $mlpf); + $charsetConverter = $this->getMock('ZBateson\StreamDecorators\Util\CharsetConverter', ['__toString']); + $pf = $this->getMock('ZBateson\MailMimeParser\Header\Part\HeaderPartFactory', ['__toString'], [$charsetConverter]); + $mlpf = $this->getMock('ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory', ['__toString'], [$charsetConverter]); + $this->consumerService = $this->getMock('ZBateson\MailMimeParser\Header\Consumer\ConsumerService', ['__toString'], [$pf, $mlpf]); } public function testParsing() diff --git a/tests/MailMimeParser/IntegrationTests/EmailFunctionalTest.php b/tests/MailMimeParser/IntegrationTests/EmailFunctionalTest.php index 14ce8593..e2d951ad 100644 --- a/tests/MailMimeParser/IntegrationTests/EmailFunctionalTest.php +++ b/tests/MailMimeParser/IntegrationTests/EmailFunctionalTest.php @@ -4,27 +4,21 @@ use PHPUnit_Framework_TestCase; use ZBateson\MailMimeParser\MailMimeParser; use ZBateson\MailMimeParser\Message; +use ZBateson\MailMimeParser\Message\Part\MimePart; +use GuzzleHttp\Psr7; /** * Description of EmailFunctionalTest * * @group Functional * @group EmailFunctionalTest - * @covers ZBateson\MailMimeParser\Stream\Base64DecodeStreamFilter - * @covers ZBateson\MailMimeParser\Stream\Base64EncodeStreamFilter - * @covers ZBateson\MailMimeParser\Stream\CharsetStreamFilter - * @covers ZBateson\MailMimeParser\Stream\ConvertStreamFilter - * @covers ZBateson\MailMimeParser\Stream\UUDecodeStreamFilter - * @covers ZBateson\MailMimeParser\Stream\UUEncodeStreamFilter - * @covers ZBateson\MailMimeParser\Message - * @covers ZBateson\MailMimeParser\Message\MimePart * @author Zaahid Bateson */ class EmailFunctionalTest extends PHPUnit_Framework_TestCase { private $parser; private $messageDir; - + // useful for testing an actual signed message with external tools -- the // tests may actually fail with this set to true though, as it always // tries to sign rather than verify a signature @@ -120,19 +114,16 @@ private function runEmailTestForMessage($message, array $props, $failMessage) $this->assertEquals($props['attachments'], $message->getAttachmentCount(), $failMessage); $attachments = $message->getAllAttachmentParts(); foreach ($attachments as $attachment) { - $name = $attachment->getHeaderParameter('Content-Type', 'name'); - if (empty($name)) { - $name = $attachment->getHeaderParameter('Content-Disposition', 'filename'); - } + $name = $attachment->getFilename(); if (!empty($name) && file_exists($this->messageDir . '/files/' . $name)) { - if ($attachment->getHeaderValue('Content-Type') === 'text/html') { + if ($attachment->getContentType() === 'text/html') { $this->assertHtmlContentTypeEquals( $name, $attachment->getContentResourceHandle(), 'HTML content is not equal' ); - } elseif (stripos($attachment->getHeaderValue('Content-Type'), 'text/') === 0) { + } elseif ($attachment->isTextPart()) { $this->assertTextContentTypeEquals( $name, $attachment->getContentResourceHandle(), @@ -160,7 +151,7 @@ private function runEmailTestForMessage($message, array $props, $failMessage) $this->runPartsTests($message, $props['parts'], $failMessage); } } - + private function runPartsTests($part, array $types, $failMessage) { $this->assertNotNull($part, $failMessage); @@ -169,9 +160,10 @@ private function runPartsTests($part, array $types, $failMessage) if (is_array($type)) { $this->assertEquals( strtolower($key), - strtolower($part->getHeaderValue('Content-Type', 'text/plain')), + $part->getContentType(), $failMessage ); + $this->assertInstanceOf('ZBateson\MailMimeParser\Message\Part\MimePart', $part); $cparts = $part->getChildParts(); $curPart = current($cparts); $this->assertCount(count($type), $cparts, $failMessage); @@ -180,10 +172,12 @@ private function runPartsTests($part, array $types, $failMessage) $curPart = next($cparts); } } else { - $this->assertEmpty($part->getChildParts(), $failMessage); + if ($part instanceof MimePart) { + $this->assertEmpty($part->getChildParts(), $failMessage); + } $this->assertEquals( strtolower($type), - strtolower($part->getHeaderValue('Content-Type', 'text/plain')), + strtolower($part->getContentType()), $failMessage ); } @@ -199,6 +193,12 @@ private function runEmailTest($key, array $props) { $this->runEmailTestForMessage($message, $props, $failMessage); $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/$key", 'w+'); + + $parts = $message->getAllParts(); + foreach ($parts as $part) { + $part->markAsChanged(); + } + $message->save($tmpSaved); rewind($tmpSaved); @@ -851,27 +851,24 @@ public function testParseEmailm1009() } /* - * m1010.txt looks like it's badly encoded. Was it really sent like that? + * m1010.txt the encoding is wrong, using setCharsetOverride */ - /* public function testParseEmailm1010() { - $this->runEmailTest('m1010', [ - 'From' => [ - 'name' => 'Doug Sauder', - 'email' => 'dwsauder@example.com' - ], - 'To' => [ - 'name' => 'Joe Blow', - 'email' => 'blow@example.com' - ], - 'Subject' => 'Test message from Netscape Communicator 4.7', - 'text' => 'HasenundFrosche.txt', - ]); - }*/ + $handle = fopen($this->messageDir . '/m1010.txt', 'r'); + $message = $this->parser->parse($handle); + fclose($handle); + + $failMessage = 'Failed while parsing m1010'; + $message->setCharsetOverride('iso-8859-1'); + $f = $message->getTextStream(0); + $this->assertNotNull($f, $failMessage); + $this->assertTextContentTypeEquals('HasenundFrosche.txt', $f, $failMessage); + } /* * m1011.txt looks like it's badly encoded. Was it really sent like that? + * Can't find what the file could be... */ /* public function testParseEmailm1011() @@ -934,7 +931,8 @@ public function testParseEmailm1014() 'email' => 'blow@example.com' ], 'Subject' => 'Test message from Netscape Communicator 4.7', - 'text' => 'hareandtortoise.txt' + 'text' => 'hareandtortoise.txt', + 'attachments' => 3 ]); } @@ -1310,7 +1308,7 @@ public function testParseEmailm3004() 'email' => 'blow@example.com' ], 'Subject' => 'Die Hasen und die Frösche', - // 'attachments' => 1, filename part is weird + 'attachments' => 1, ]); } @@ -1365,55 +1363,6 @@ public function testParseEmailm3007() ]); } - public function testRewriteEmailContentm0001() - { - $handle = fopen($this->messageDir . '/m0001.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $content = $message->getTextPart(); - $content->setRawHeader('Content-Type', "text/html;\r\n\tcharset=\"iso-8859-1\""); - $test = 'This is my simple test'; - $content->setContent($test); - - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/rewrite_m0001", 'w+'); - $message->save($tmpSaved); - rewind($tmpSaved); - - $messageWritten = $this->parser->parse($tmpSaved); - fclose($tmpSaved); - $c2 = $messageWritten->getHtmlPart(); - $this->assertEquals($test, $c2->getContent()); - } - - public function testRewriteEmailAttachmentm2004() - { - $handle = fopen($this->messageDir . '/m2004.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $att = $message->getAttachmentPart(0); - $att->setRawHeader( - 'Content-Disposition', - $att->getHeaderValue('Content-Disposition') . '; filename="greenball.png"' - ); - $green = fopen($this->messageDir . '/files/greenball.png', 'r'); - $att->attachContentResourceHandle($green); - - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/rewrite_m2004", 'w+'); - $message->save($tmpSaved); - rewind($tmpSaved); - - $messageWritten = $this->parser->parse($tmpSaved); - fclose($tmpSaved); - $a2 = $messageWritten->getAttachmentPart(0); - $this->assertEquals($a2->getHeaderParameter('Content-Disposition', 'filename'), 'greenball.png'); - $this->assertEquals( - file_get_contents($this->messageDir . '/files/greenball.png'), - $a2->getContent() - ); - } - public function testParseFromStringm0001() { $str = file_get_contents($this->messageDir . '/m0001.txt'); @@ -1432,71 +1381,82 @@ public function testParseFromStringm0001() ], 'Failed to parse m0001 from a string'); } - public function testRemoveAttachmentPartm0013() + public function testVerifySignedEmailm4001() { - $handle = fopen($this->messageDir . '/m0013.txt', 'r'); + $handle = fopen($this->messageDir . '/m4001.txt', 'r'); $message = $this->parser->parse($handle); fclose($handle); - $props = [ + $testString = $message->getSignedMessageAsString(); + $this->assertEquals(md5($testString), trim($message->getSignaturePart()->getContent())); + } + + public function testParseEmailm4001() + { + $this->runEmailTest('m4001', [ 'From' => [ 'name' => 'Doug Sauder', 'email' => 'doug@example.com' ], 'To' => [ - 'name' => 'Joe Blow', - 'email' => 'jblow@example.com' + 'name' => 'Jürgen Schmürgen', + 'email' => 'schmuergen@example.com' ], - 'Subject' => 'Test message from Microsoft Outlook 00', - 'attachments' => 2 - ]; - - $message->removeAttachmentPart(0); - - $test1 = $props; - $test1['attachments'] = 1; - - $this->assertEquals(1, $message->getAttachmentCount()); - $att = $message->getAttachmentPart(0); - $this->assertEquals('redball.png', $att->getHeaderParameter('Content-Disposition', 'filename')); - $this->runEmailTestForMessage($message, $test1, 'failed removing content parts from m0013'); + 'Subject' => 'Die Hasen und die Frösche (Microsoft Outlook 00)', + 'text' => 'HasenundFrosche.txt', + 'signed' => [ + 'protocol' => 'application/pgp-signature', + 'micalg' => 'pgp-sha256', + 'body' => '9825cba003a7ac85b9a3f3dc9f8423fd' + ], + ]); } - public function testRemoveContentPartsm0014() + public function testVerifySignedEmailm4002() { - $handle = fopen($this->messageDir . '/m0014.txt', 'r'); + $handle = fopen($this->messageDir . '/m4002.txt', 'r'); $message = $this->parser->parse($handle); fclose($handle); - $message->removeTextPart(); + $testString = $message->getSignedMessageAsString(); + $this->assertEquals(md5($testString), trim($message->getSignaturePart()->getContent())); + } - $props = [ + public function testParseEmailm4002() + { + $this->runEmailTest('m4002', [ 'From' => [ 'name' => 'Doug Sauder', 'email' => 'doug@example.com' ], 'To' => [ - 'name' => 'Joe Blow', - 'email' => 'jblow@example.com' + 'name' => 'Heinz Müller', + 'email' => 'mueller@example.com' ], 'Subject' => 'Test message from Microsoft Outlook 00', 'text' => 'hareandtortoise.txt', - 'html' => 'hareandtortoise.txt', - ]; - - $test1 = $props; - unset($test1['text']); - $this->assertNull($message->getTextPart()); - $this->runEmailTestForMessage($message, $test1, 'failed removing content parts from m0014'); + 'attachments' => 3, + 'signed' => [ + 'protocol' => 'application/pgp-signature', + 'micalg' => 'md5', + 'body' => 'f691886408cbeedc753548d2d198bf92' + ], + ]); } - public function testRemoveTextPartm0020() + public function testVerifySignedEmailm4003() { - $handle = fopen($this->messageDir . '/m0020.txt', 'r'); + $handle = fopen($this->messageDir . '/m4003.txt', 'r'); $message = $this->parser->parse($handle); fclose($handle); - $props = [ + $testString = $message->getSignedMessageAsString(); + $this->assertEquals(md5($testString), trim($message->getSignaturePart()->getContent())); + } + + public function testParseEmailm4003() + { + $this->runEmailTest('m4003', [ 'From' => [ 'name' => 'Doug Sauder', 'email' => 'doug@example.com' @@ -1508,84 +1468,351 @@ public function testRemoveTextPartm0020() 'Subject' => 'Test message from Microsoft Outlook 00', 'text' => 'hareandtortoise.txt', 'html' => 'hareandtortoise.txt', - 'attachments' => 2, - ]; - - $test1 = $props; - unset($test1['text']); + 'signed' => [ + 'protocol' => 'application/pgp-signature', + 'micalg' => 'pgp-sha256', + 'body' => 'ba0ce5fac600d1a2e1f297d0040b858c' + ], + ]); + } - $message->removeTextPart(); - $this->assertNull($message->getTextPart()); - $this->runEmailTestForMessage($message, $test1, 'failed removing text part from m0020'); + public function testVerifySignedEmailm4004() + { + $handle = fopen($this->messageDir . '/m4004.txt', 'r'); + $message = $this->parser->parse($handle); + fclose($handle); - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/rm_m0020", 'w+'); - $message->save($tmpSaved); - rewind($tmpSaved); + $testString = $message->getSignedMessageAsString(); + $this->assertEquals(md5($testString), trim($message->getSignaturePart()->getContent())); + } - $messageWritten = $this->parser->parse($tmpSaved); - fclose($tmpSaved); - $failMessage = 'Failed while parsing saved message for rm_m0020'; - $this->runEmailTestForMessage($messageWritten, $test1, $failMessage); + public function testParseEmailm4004() + { + $this->runEmailTest('m4004', [ + 'From' => [ + 'name' => 'Doug Sauder', + 'email' => 'dwsauder@example.com' + ], + 'To' => [ + 'name' => 'Heinz Müller', + 'email' => 'mueller@example.com' + ], + 'Subject' => 'Die Hasen und die Frösche (Netscape Messenger 4.7)', + 'html' => 'HasenundFrosche.txt', + 'attachments' => 4, + 'signed' => [ + 'protocol' => 'application/pgp-signature', + 'micalg' => 'pgp-sha256', + 'body' => 'eb4c0347d13a2bf71a3f9673c4b5e3db' + ], + ]); } - public function testRemoveAllHtmlPartsm0020() + public function testParseEmailm4005() { - $handle = fopen($this->messageDir . '/m0020.txt', 'r'); + $handle = fopen($this->messageDir . '/m4005.txt', 'r'); $message = $this->parser->parse($handle); fclose($handle); + $str = file_get_contents($this->messageDir . '/files/blueball.png'); + $this->assertEquals(1, $message->getAttachmentCount()); + $this->assertEquals('text/rtf', $message->getAttachmentPart(0)->getHeaderValue('Content-Type')); + $this->assertTrue($str === $message->getAttachmentPart(0)->getContent(), 'text/rtf stream doesn\'t match binary stream'); + $props = [ 'From' => [ 'name' => 'Doug Sauder', 'email' => 'doug@example.com' ], 'To' => [ - 'name' => 'Joe Blow', - 'email' => 'jblow@example.com' + 'name' => 'Heinz Müller', + 'email' => 'mueller@example.com' ], 'Subject' => 'Test message from Microsoft Outlook 00', - 'text' => 'hareandtortoise.txt', - 'html' => 'hareandtortoise.txt', - 'attachments' => 2, + 'text' => 'hareandtortoise.txt' ]; - $test1 = $props; - unset($test1['html']); - - $message->removeAllHtmlParts(); - $this->assertNull($message->getHtmlPart()); - $this->runEmailTestForMessage($message, $test1, 'failed removing content parts from m0020'); - - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/rmh_m0020", 'w+'); + $this->runEmailTestForMessage($message, $props, 'failed parsing m4005'); + $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/m4005", 'w+'); $message->save($tmpSaved); rewind($tmpSaved); $messageWritten = $this->parser->parse($tmpSaved); fclose($tmpSaved); - $failMessage = 'Failed while parsing saved message for rmh_m0020'; - $this->runEmailTestForMessage($messageWritten, $test1, $failMessage); + $failMessage = 'Failed while parsing saved message for adding a large attachment to m0001'; + $this->runEmailTestForMessage($messageWritten, $props, $failMessage); + + $this->assertEquals(1, $messageWritten->getAttachmentCount()); + $this->assertEquals('text/rtf', $messageWritten->getAttachmentPart(0)->getHeaderValue('Content-Type')); + $this->assertTrue($str === $messageWritten->getAttachmentPart(0)->getContent(), 'text/rtf stream doesn\'t match binary stream'); } - - public function testRemoveHtmlPartm0020() - { - $handle = fopen($this->messageDir . '/m0020.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - $props = [ + public function testParseEmailm4006() + { + $this->runEmailTest('m4006', [ 'From' => [ - 'name' => 'Doug Sauder', - 'email' => 'doug@example.com' + 'name' => 'Test Sender', + 'email' => 'sender@email.test' ], 'To' => [ - 'name' => 'Joe Blow', - 'email' => 'jblow@example.com' + 'name' => 'Test Recipient', + 'email' => 'recipient@email.test' ], - 'Subject' => 'Test message from Microsoft Outlook 00', - 'text' => 'hareandtortoise.txt', - 'html' => 'hareandtortoise.txt', - 'attachments' => 2, - ]; + 'Subject' => 'Read: invitation', + 'attachments' => 1, + ]); + } + + public function testParseEmailm4007() + { + $this->runEmailTest('m4007', [ + 'From' => [ + 'name' => 'Test Sender', + 'email' => 'sender@email.test' + ], + 'To' => [ + 'name' => 'Test Recipient', + 'email' => 'recipient@email.test' + ], + 'Subject' => 'Test multipart-digest', + 'attachments' => 1, + ]); + } + + public function testVerifySignedEmailm4008() + { + $handle = fopen($this->messageDir . '/m4008.txt', 'r'); + $message = $this->parser->parse($handle); + fclose($handle); + + $testString = $message->getSignedMessageAsString(); + $this->assertEquals(md5($testString), trim($message->getSignaturePart()->getContent())); + } + + public function testParseEmailm4008() + { + $this->runEmailTest('m4008', [ + 'From' => [ + 'name' => 'Doug Sauder', + 'email' => 'dwsauder@example.com' + ], + 'To' => [ + 'name' => 'Heinz Müller', + 'email' => 'mueller@example.com' + ], + 'Subject' => 'Die Hasen und die Frösche (Netscape Messenger 4.7)', + 'signed' => [ + 'protocol' => 'application/x-pgp-signature', + 'signed-part-protocol' => 'application/pgp-signature', + 'micalg' => 'pgp-sha256', + 'body' => '9f5c560f86b607c9087b84e9baa98189' + ], + ]); + } + + public function testRewriteEmailContentm0001() + { + $handle = fopen($this->messageDir . '/m0001.txt', 'r'); + $message = $this->parser->parse($handle); + fclose($handle); + + $content = $message->getTextPart(); + $content->setRawHeader('Content-Type', "text/html;\r\n\tcharset=\"iso-8859-1\""); + $test = 'This is my simple test'; + $content->setContent($test); + + $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/rewrite_m0001", 'w+'); + $message->save($tmpSaved); + rewind($tmpSaved); + + $messageWritten = $this->parser->parse($tmpSaved); + fclose($tmpSaved); + $c2 = $messageWritten->getHtmlPart(); + $this->assertEquals($test, $c2->getContent()); + } + + public function testRewriteEmailAttachmentm2004() + { + $handle = fopen($this->messageDir . '/m2004.txt', 'r'); + $message = $this->parser->parse($handle); + fclose($handle); + + $att = $message->getAttachmentPart(0); + $att->setRawHeader( + 'Content-Disposition', + $att->getHeaderValue('Content-Disposition') . '; filename="greenball.png"' + ); + $green = fopen($this->messageDir . '/files/greenball.png', 'r'); + $att->attachContentStream(Psr7\stream_for($green)); + + $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/rewrite_m2004", 'w+'); + $message->save($tmpSaved); + rewind($tmpSaved); + + $messageWritten = $this->parser->parse($tmpSaved); + fclose($tmpSaved); + $a2 = $messageWritten->getAttachmentPart(0); + $this->assertEquals($a2->getHeaderParameter('Content-Disposition', 'filename'), 'greenball.png'); + $this->assertEquals( + file_get_contents($this->messageDir . '/files/greenball.png'), + $a2->getContent() + ); + } + + public function testRemoveAttachmentPartm0013() + { + $handle = fopen($this->messageDir . '/m0013.txt', 'r'); + $message = $this->parser->parse($handle); + fclose($handle); + + $props = [ + 'From' => [ + 'name' => 'Doug Sauder', + 'email' => 'doug@example.com' + ], + 'To' => [ + 'name' => 'Joe Blow', + 'email' => 'jblow@example.com' + ], + 'Subject' => 'Test message from Microsoft Outlook 00', + 'attachments' => 2 + ]; + + $message->removeAttachmentPart(0); + + $test1 = $props; + $test1['attachments'] = 1; + + $this->assertEquals(1, $message->getAttachmentCount()); + $att = $message->getAttachmentPart(0); + $this->assertEquals('redball.png', $att->getHeaderParameter('Content-Disposition', 'filename')); + $this->runEmailTestForMessage($message, $test1, 'failed removing content parts from m0013'); + } + + public function testRemoveContentPartsm0014() + { + $handle = fopen($this->messageDir . '/m0014.txt', 'r'); + $message = $this->parser->parse($handle); + fclose($handle); + + $message->removeTextPart(); + + $props = [ + 'From' => [ + 'name' => 'Doug Sauder', + 'email' => 'doug@example.com' + ], + 'To' => [ + 'name' => 'Joe Blow', + 'email' => 'jblow@example.com' + ], + 'Subject' => 'Test message from Microsoft Outlook 00', + 'text' => 'hareandtortoise.txt', + 'html' => 'hareandtortoise.txt', + ]; + + $test1 = $props; + unset($test1['text']); + $this->assertNull($message->getTextPart()); + $this->runEmailTestForMessage($message, $test1, 'failed removing content parts from m0014'); + } + + public function testRemoveTextPartm0020() + { + $handle = fopen($this->messageDir . '/m0020.txt', 'r'); + $message = $this->parser->parse($handle); + fclose($handle); + + $props = [ + 'From' => [ + 'name' => 'Doug Sauder', + 'email' => 'doug@example.com' + ], + 'To' => [ + 'name' => 'Joe Blow', + 'email' => 'jblow@example.com' + ], + 'Subject' => 'Test message from Microsoft Outlook 00', + 'text' => 'hareandtortoise.txt', + 'html' => 'hareandtortoise.txt', + 'attachments' => 2, + ]; + + $test1 = $props; + unset($test1['text']); + + $message->removeTextPart(); + $this->assertNull($message->getTextPart()); + $this->runEmailTestForMessage($message, $test1, 'failed removing text part from m0020'); + + $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/rm_m0020", 'w+'); + $message->save($tmpSaved); + rewind($tmpSaved); + + $messageWritten = $this->parser->parse($tmpSaved); + fclose($tmpSaved); + $failMessage = 'Failed while parsing saved message for rm_m0020'; + $this->runEmailTestForMessage($messageWritten, $test1, $failMessage); + } + + public function testRemoveAllHtmlPartsm0020() + { + $handle = fopen($this->messageDir . '/m0020.txt', 'r'); + $message = $this->parser->parse($handle); + fclose($handle); + + $props = [ + 'From' => [ + 'name' => 'Doug Sauder', + 'email' => 'doug@example.com' + ], + 'To' => [ + 'name' => 'Joe Blow', + 'email' => 'jblow@example.com' + ], + 'Subject' => 'Test message from Microsoft Outlook 00', + 'text' => 'hareandtortoise.txt', + 'html' => 'hareandtortoise.txt', + 'attachments' => 2, + ]; + + $test1 = $props; + unset($test1['html']); + + $message->removeAllHtmlParts(); + $this->assertNull($message->getHtmlPart()); + $this->runEmailTestForMessage($message, $test1, 'failed removing content parts from m0020'); + + $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/rmh_m0020", 'w+'); + $message->save($tmpSaved); + rewind($tmpSaved); + + $messageWritten = $this->parser->parse($tmpSaved); + fclose($tmpSaved); + $failMessage = 'Failed while parsing saved message for rmh_m0020'; + $this->runEmailTestForMessage($messageWritten, $test1, $failMessage); + } + + public function testRemoveHtmlPartm0020() + { + $handle = fopen($this->messageDir . '/m0020.txt', 'r'); + $message = $this->parser->parse($handle); + fclose($handle); + + $props = [ + 'From' => [ + 'name' => 'Doug Sauder', + 'email' => 'doug@example.com' + ], + 'To' => [ + 'name' => 'Joe Blow', + 'email' => 'jblow@example.com' + ], + 'Subject' => 'Test message from Microsoft Outlook 00', + 'text' => 'hareandtortoise.txt', + 'html' => 'hareandtortoise.txt', + 'attachments' => 2, + ]; $test1 = $props; unset($test1['html']); @@ -1595,7 +1822,7 @@ public function testRemoveHtmlPartm0020() $thirdHtmlPart = $message->getHtmlPart(2); $secondContent = $secondHtmlPart->getContent(); - + $message->removeHtmlPart(); $this->assertNotNull($message->getHtmlPart()); $this->assertNotEquals($firstHtmlPart, $message->getHtmlPart()); @@ -1899,148 +2126,9 @@ public function testAddAttachmentPartm0001() $this->runEmailTestForMessage($messageWritten, $props, $failMessage); } - public function testAddAttachmentPartm0011() + public function testAddLargeAttachmentPartm0001() { - $handle = fopen($this->messageDir . '/m0011.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $message->addAttachmentPart( - file_get_contents($this->messageDir . '/files/farmerandstork.txt'), - 'text/plain', - 'farmerandstork.txt' - ); - - $props = [ - 'From' => [ - 'name' => 'Doug Sauder', - 'email' => 'doug@example.com' - ], - 'To' => [ - 'name' => 'Heinz Müller', - 'email' => 'mueller@example.com' - ], - 'Subject' => 'Test message from Microsoft Outlook 00', - 'text' => 'hareandtortoise.txt', - 'attachments' => 4, - 'parts' => [ - 'multipart/mixed' => [ - 'text/plain', - 'image/png', - 'image/png', - 'image/png', - 'text/plain' - ] - ], - ]; - - $this->runEmailTestForMessage($message, $props, 'failed adding attachment part to m0011'); - - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/att_m0011", 'w+'); - $message->save($tmpSaved); - rewind($tmpSaved); - - $messageWritten = $this->parser->parse($tmpSaved); - fclose($tmpSaved); - $failMessage = 'Failed while parsing saved message for added attachment to m0001'; - $this->runEmailTestForMessage($messageWritten, $props, $failMessage); - - $message->addAttachmentPartFromFile( - $this->messageDir . '/files/redball.png', - 'image/png', - 'redball-2.png' - ); - $props['attachments'] = 5; - $props['parts']['multipart/mixed'][] = 'image/png'; - - // due to what seems to be a bug in hhvm, after stream_copy_to_stream is - // called in MimePart::copyContentStream, the CharsetStreamFilter filter - // is no longer called on the stream, resulting in a failure here on the - // next test - //$this->runEmailTestForMessage($message, $props, 'failed adding second attachment part to m0001'); - - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/att2_m0011", 'w+'); - $message->save($tmpSaved); - rewind($tmpSaved); - - $messageWritten = $this->parser->parse($tmpSaved); - fclose($tmpSaved); - $failMessage = 'Failed while parsing saved message for second added attachment to m0011'; - $this->runEmailTestForMessage($messageWritten, $props, $failMessage); - } - - public function testAddAttachmentPartm0014() - { - $handle = fopen($this->messageDir . '/m0014.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $message->addAttachmentPart( - file_get_contents($this->messageDir . '/files/blueball.png'), - 'image/png', - 'blueball.png' - ); - - $props = [ - 'From' => [ - 'name' => 'Doug Sauder', - 'email' => 'doug@example.com' - ], - 'To' => [ - 'name' => 'Joe Blow', - 'email' => 'jblow@example.com' - ], - 'Subject' => 'Test message from Microsoft Outlook 00', - 'text' => 'hareandtortoise.txt', - 'html' => 'hareandtortoise.txt', - 'parts' => [ - 'multipart/mixed' => [ - 'multipart/alternative' => [ - 'text/plain', - 'text/html' - ], - 'image/png' - ] - ] - ]; - - $this->runEmailTestForMessage($message, $props, 'failed adding attachment part to m0014'); - - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/att_m0014", 'w+'); - $message->save($tmpSaved); - rewind($tmpSaved); - - $messageWritten = $this->parser->parse($tmpSaved); - fclose($tmpSaved); - $failMessage = 'Failed while parsing saved message for added attachment to m0014'; - $this->runEmailTestForMessage($messageWritten, $props, $failMessage); - - $message->addAttachmentPartFromFile( - $this->messageDir . '/files/redball.png', - 'image/png', - 'redball.png' - ); - $props['parts']['multipart/mixed'][] = 'image/png'; - - // due to what seems to be a bug in hhvm, after stream_copy_to_stream is - // called in MimePart::copyContentStream, the CharsetStreamFilter filter - // is no longer called on the stream, resulting in a failure here on the - // next test - //$this->runEmailTestForMessage($message, $props, 'failed adding second attachment part to m0001'); - - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/att2_m0014", 'w+'); - $message->save($tmpSaved); - rewind($tmpSaved); - - $messageWritten = $this->parser->parse($tmpSaved); - fclose($tmpSaved); - $failMessage = 'Failed while parsing saved message for second added attachment to m0014'; - $this->runEmailTestForMessage($messageWritten, $props, $failMessage); - } - - public function testAddLargeAttachmentPartm0001() - { - $handle = fopen($this->messageDir . '/m0001.txt', 'r'); + $handle = fopen($this->messageDir . '/m0001.txt', 'r'); $message = $this->parser->parse($handle); fclose($handle); @@ -2083,13 +2171,13 @@ public function testCreateSignedPartm0001() $this->assertNull($message->getHtmlPart()); $message->setAsMultipartSigned('pgp-sha256', 'application/pgp-signature'); - $signableContent = $message->getSignableBody(); + $signableContent = $message->getSignedMessageAsString(); //$signature = md5($signableContent); file_put_contents(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sigpart_m0001", $signableContent); $signature = $this->getSignatureForContent($signableContent); - $message->createSignaturePart($signature); + $message->setSignature($signature); $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sig_m0001", 'w+'); $message->save($tmpSaved); @@ -2106,11 +2194,10 @@ public function testCreateSignedPartm0001() fclose($tmpSaved); $failMessage = 'Failed while parsing saved message for added HTML content to m0001'; - $testString = $messageWritten->getOriginalMessageStringForSignatureVerification(); + $testString = $messageWritten->getSignedMessageAsString(); $this->assertEquals($signableContent, $testString); - $this->assertEquals($this->getSignatureForContent($testString), $signature); - + $props = [ 'From' => [ 'name' => 'Doug Sauder', @@ -2139,12 +2226,12 @@ public function testCreateSignedPartm0014() $message->setAsMultipartSigned('pgp-sha256', 'application/pgp-signature'); - $this->assertEquals('text/html', $message->getContentPart()->getChild(0)->getHeaderValue('Content-Type')); - $signableContent = $message->getSignableBody(); + $this->assertEquals('text/html', $message->getHtmlPart()->getHeaderValue('Content-Type')); + $signableContent = $message->getSignedMessageAsString(); file_put_contents(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sigpart_m0014", $signableContent); $signature = $this->getSignatureForContent($signableContent); - $message->createSignaturePart($signature); + $message->setSignature($signature); $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sig_m0014", 'w+'); $message->save($tmpSaved); @@ -2156,8 +2243,8 @@ public function testCreateSignedPartm0014() $messageWritten = $this->parser->parse($tmpSaved); fclose($tmpSaved); $failMessage = 'Failed while parsing saved message for added HTML content to m0014'; - - $testString = $messageWritten->getOriginalMessageStringForSignatureVerification(); + + $testString = $messageWritten->getSignedMessageAsString(); $this->assertEquals($this->getSignatureForContent($testString), $signature); $props = [ @@ -2193,10 +2280,10 @@ public function testCreateSignedPartm0015() $this->assertEquals(2, $message->getChildCount()); $this->assertEquals('multipart/mixed', strtolower($message->getChild(0)->getHeaderValue('Content-Type'))); - $signableContent = $message->getSignableBody(); + $signableContent = $message->getSignedMessageAsString(); file_put_contents(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sigpart_m0015", $signableContent); $signature = $this->getSignatureForContent($signableContent); - $message->createSignaturePart($signature); + $message->setSignature($signature); $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sig_m0015", 'w+'); $message->save($tmpSaved); @@ -2208,8 +2295,8 @@ public function testCreateSignedPartm0015() $messageWritten = $this->parser->parse($tmpSaved); fclose($tmpSaved); $failMessage = 'Failed while parsing saved message for added HTML content to m0015'; - - $testString = $messageWritten->getOriginalMessageStringForSignatureVerification(); + + $testString = $messageWritten->getSignedMessageAsString(); $this->assertEquals($this->getSignatureForContent($testString), $signature); $props = [ @@ -2243,11 +2330,11 @@ public function testCreateSignedPartm0018() $this->assertNull($message->getHtmlPart()); $message->setAsMultipartSigned('pgp-sha256', 'application/pgp-signature'); - $signableContent = $message->getSignableBody(); + $signableContent = $message->getSignedMessageAsString(); file_put_contents(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sigpart_m0018", $signableContent); $signature = $this->getSignatureForContent($signableContent); - $message->createSignaturePart($signature); + $message->setSignature($signature); $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sig_m0018", 'w+'); $message->save($tmpSaved); @@ -2259,8 +2346,8 @@ public function testCreateSignedPartm0018() $messageWritten = $this->parser->parse($tmpSaved); fclose($tmpSaved); $failMessage = 'Failed while parsing saved message for added HTML content to m0018'; - - $testString = $messageWritten->getOriginalMessageStringForSignatureVerification(); + + $testString = $messageWritten->getSignedMessageAsString(); $this->assertEquals($this->getSignatureForContent($testString), $signature); $props = [ @@ -2292,11 +2379,11 @@ public function testCreateSignedPartm0019() $this->assertNotNull($message->getHtmlPart()); $message->setAsMultipartSigned('pgp-sha256', 'application/pgp-signature'); - $signableContent = $message->getSignableBody(); + $signableContent = $message->getSignedMessageAsString(); file_put_contents(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sigpart_m0019", $signableContent); $signature = $this->getSignatureForContent($signableContent); - $message->createSignaturePart($signature); + $message->setSignature($signature); $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sig_m0019", 'w+'); $message->save($tmpSaved); @@ -2308,8 +2395,8 @@ public function testCreateSignedPartm0019() $messageWritten = $this->parser->parse($tmpSaved); fclose($tmpSaved); $failMessage = 'Failed while parsing saved message for added HTML content to signed part sig_m0019'; - - $testString = $messageWritten->getOriginalMessageStringForSignatureVerification(); + + $testString = $messageWritten->getSignedMessageAsString(); $this->assertEquals($this->getSignatureForContent($testString), $signature); $props = [ @@ -2341,11 +2428,11 @@ public function testCreateSignedPartm1005() fclose($handle); $message->setAsMultipartSigned('pgp-sha256', 'application/pgp-signature'); - $signableContent = $message->getSignableBody(); + $signableContent = $message->getSignedMessageAsString(); file_put_contents(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sigpart_m1005", $signableContent); $signature = $this->getSignatureForContent($signableContent); - $message->createSignaturePart($signature); + $message->setSignature($signature); $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sig_m1005", 'w+'); $message->save($tmpSaved); @@ -2357,8 +2444,8 @@ public function testCreateSignedPartm1005() $messageWritten = $this->parser->parse($tmpSaved); fclose($tmpSaved); $failMessage = 'Failed while parsing saved message for added HTML content to m1005'; - - $testString = $messageWritten->getOriginalMessageStringForSignatureVerification(); + + $testString = $messageWritten->getSignedMessageAsString(); $this->assertEquals($this->getSignatureForContent($testString), $signature); $props = [ @@ -2382,382 +2469,4 @@ public function testCreateSignedPartm1005() $this->runEmailTestForMessage($messageWritten, $props, $failMessage); } - - public function testVerifySignedEmailm4001() - { - $handle = fopen($this->messageDir . '/m4001.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $testString = $message->getOriginalMessageStringForSignatureVerification(); - $this->assertEquals(md5($testString), trim($message->getSignaturePart()->getContent())); - } - - public function testParseEmailm4001() - { - $this->runEmailTest('m4001', [ - 'From' => [ - 'name' => 'Doug Sauder', - 'email' => 'doug@example.com' - ], - 'To' => [ - 'name' => 'Jürgen Schmürgen', - 'email' => 'schmuergen@example.com' - ], - 'Subject' => 'Die Hasen und die Frösche (Microsoft Outlook 00)', - 'text' => 'HasenundFrosche.txt', - 'signed' => [ - 'protocol' => 'application/pgp-signature', - 'micalg' => 'pgp-sha256', - 'body' => '9825cba003a7ac85b9a3f3dc9f8423fd' - ], - ]); - } - - public function testVerifySignedEmailm4002() - { - $handle = fopen($this->messageDir . '/m4002.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $testString = $message->getOriginalMessageStringForSignatureVerification(); - $this->assertEquals(md5($testString), trim($message->getSignaturePart()->getContent())); - } - - public function testParseEmailm4002() - { - $this->runEmailTest('m4002', [ - 'From' => [ - 'name' => 'Doug Sauder', - 'email' => 'doug@example.com' - ], - 'To' => [ - 'name' => 'Heinz Müller', - 'email' => 'mueller@example.com' - ], - 'Subject' => 'Test message from Microsoft Outlook 00', - 'text' => 'hareandtortoise.txt', - 'attachments' => 3, - 'signed' => [ - 'protocol' => 'application/pgp-signature', - 'micalg' => 'md5', - 'body' => 'f691886408cbeedc753548d2d198bf92' - ], - ]); - } - - public function testVerifySignedEmailm4003() - { - $handle = fopen($this->messageDir . '/m4003.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $testString = $message->getOriginalMessageStringForSignatureVerification(); - $this->assertEquals(md5($testString), trim($message->getSignaturePart()->getContent())); - } - - public function testParseEmailm4003() - { - $this->runEmailTest('m4003', [ - 'From' => [ - 'name' => 'Doug Sauder', - 'email' => 'doug@example.com' - ], - 'To' => [ - 'name' => 'Joe Blow', - 'email' => 'jblow@example.com' - ], - 'Subject' => 'Test message from Microsoft Outlook 00', - 'text' => 'hareandtortoise.txt', - 'html' => 'hareandtortoise.txt', - 'signed' => [ - 'protocol' => 'application/pgp-signature', - 'micalg' => 'pgp-sha256', - 'body' => 'ba0ce5fac600d1a2e1f297d0040b858c' - ], - ]); - } - - public function testVerifySignedEmailm4004() - { - $handle = fopen($this->messageDir . '/m4004.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $testString = $message->getOriginalMessageStringForSignatureVerification(); - $this->assertEquals(md5($testString), trim($message->getSignaturePart()->getContent())); - } - - public function testParseEmailm4004() - { - $this->runEmailTest('m4004', [ - 'From' => [ - 'name' => 'Doug Sauder', - 'email' => 'dwsauder@example.com' - ], - 'To' => [ - 'name' => 'Heinz Müller', - 'email' => 'mueller@example.com' - ], - 'Subject' => 'Die Hasen und die Frösche (Netscape Messenger 4.7)', - 'html' => 'HasenundFrosche.txt', - 'attachments' => 4, - 'signed' => [ - 'protocol' => 'application/pgp-signature', - 'micalg' => 'pgp-sha256', - 'body' => 'eb4c0347d13a2bf71a3f9673c4b5e3db' - ], - ]); - } - - public function testParseEmailm4005() - { - $handle = fopen($this->messageDir . '/m4005.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $str = file_get_contents($this->messageDir . '/files/blueball.png'); - $this->assertEquals(1, $message->getAttachmentCount()); - $this->assertEquals('text/rtf', $message->getAttachmentPart(0)->getHeaderValue('Content-Type')); - $this->assertTrue($str === $message->getAttachmentPart(0)->getContent(), 'text/rtf stream doesn\'t match binary stream'); - - $props = [ - 'From' => [ - 'name' => 'Doug Sauder', - 'email' => 'doug@example.com' - ], - 'To' => [ - 'name' => 'Heinz Müller', - 'email' => 'mueller@example.com' - ], - 'Subject' => 'Test message from Microsoft Outlook 00', - 'text' => 'hareandtortoise.txt' - ]; - - $this->runEmailTestForMessage($message, $props, 'failed adding large attachment part to m0001'); - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/m4005", 'w+'); - $message->save($tmpSaved); - rewind($tmpSaved); - - $messageWritten = $this->parser->parse($tmpSaved); - fclose($tmpSaved); - $failMessage = 'Failed while parsing saved message for adding a large attachment to m0001'; - $this->runEmailTestForMessage($messageWritten, $props, $failMessage); - - $this->assertEquals(1, $messageWritten->getAttachmentCount()); - $this->assertEquals('text/rtf', $messageWritten->getAttachmentPart(0)->getHeaderValue('Content-Type')); - $this->assertTrue($str === $messageWritten->getAttachmentPart(0)->getContent(), 'text/rtf stream doesn\'t match binary stream'); - } - - public function testParseEmailm4006() - { - $this->runEmailTest('m4006', [ - 'From' => [ - 'name' => 'Test Sender', - 'email' => 'sender@email.test' - ], - 'To' => [ - 'name' => 'Test Recipient', - 'email' => 'recipient@email.test' - ], - 'Subject' => 'Read: invitation', - 'attachments' => 1, - ]); - } - - public function testCreateSignedPartForEmailm4006() - { - $handle = fopen($this->messageDir . '/m4006.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $message->setAsMultipartSigned('pgp-sha256', 'application/pgp-signature'); - - $signableContent = $message->getSignableBody(); - file_put_contents(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sigpart_m4006", $signableContent); - $signature = $this->getSignatureForContent($signableContent); - $message->createSignaturePart($signature); - - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sig_m4006", 'w+'); - $message->save($tmpSaved); - rewind($tmpSaved); - - $this->assertContains($signableContent, preg_replace('/\r\n|\r|\n/', "\r\n", stream_get_contents($tmpSaved))); - rewind($tmpSaved); - - $messageWritten = $this->parser->parse($tmpSaved); - fclose($tmpSaved); - $failMessage = 'Failed while parsing saved message for m4006'; - - $testString = $messageWritten->getOriginalMessageStringForSignatureVerification(); - $this->assertEquals($this->getSignatureForContent($testString), $signature); - - $props = [ - 'From' => [ - 'name' => 'Test Sender', - 'email' => 'sender@email.test' - ], - 'To' => [ - 'name' => 'Test Recipient', - 'email' => 'recipient@email.test' - ], - 'Subject' => 'Read: invitation', - 'attachments' => 1, - 'signed' => [ - 'protocol' => 'application/pgp-signature', - 'micalg' => 'pgp-sha256', - 'body' => $signature - ] - ]; - - $this->runEmailTestForMessage($messageWritten, $props, $failMessage); - } - - public function testParseEmailm4007() - { - $this->runEmailTest('m4007', [ - 'From' => [ - 'name' => 'Test Sender', - 'email' => 'sender@email.test' - ], - 'To' => [ - 'name' => 'Test Recipient', - 'email' => 'recipient@email.test' - ], - 'Subject' => 'Test multipart-digest', - 'attachments' => 1, - ]); - } - - public function testCreateSignedPartForEmailm4007() - { - $handle = fopen($this->messageDir . '/m4007.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $message->setAsMultipartSigned('pgp-sha256', 'application/pgp-signature'); - - $signableContent = $message->getSignableBody(); - file_put_contents(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sigpart_m4007", $signableContent); - $signature = $this->getSignatureForContent($signableContent); - $message->createSignaturePart($signature); - - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sig_m4007", 'w+'); - $message->save($tmpSaved); - rewind($tmpSaved); - - $this->assertContains($signableContent, stream_get_contents($tmpSaved)); - rewind($tmpSaved); - - $messageWritten = $this->parser->parse($tmpSaved); - fclose($tmpSaved); - $failMessage = 'Failed while parsing saved message for m4007'; - - $testString = $messageWritten->getOriginalMessageStringForSignatureVerification(); - $this->assertEquals($testString, $signableContent); - $this->assertEquals($this->getSignatureForContent($testString), $signature); - - $props = [ - 'From' => [ - 'name' => 'Test Sender', - 'email' => 'sender@email.test' - ], - 'To' => [ - 'name' => 'Test Recipient', - 'email' => 'recipient@email.test' - ], - 'Subject' => 'Test multipart-digest', - 'attachments' => 1, - 'signed' => [ - 'protocol' => 'application/pgp-signature', - 'micalg' => 'pgp-sha256', - 'body' => $signature - ] - ]; - - $this->runEmailTestForMessage($messageWritten, $props, $failMessage); - } - - public function testVerifySignedEmailm4008() - { - $handle = fopen($this->messageDir . '/m4008.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $testString = $message->getOriginalMessageStringForSignatureVerification(); - $this->assertEquals(md5($testString), trim($message->getSignaturePart()->getContent())); - } - - public function testParseEmailm4008() - { - $this->runEmailTest('m4008', [ - 'From' => [ - 'name' => 'Doug Sauder', - 'email' => 'doug@example.com' - ], - 'To' => [ - 'name' => 'Jürgen Schmürgen', - 'email' => 'schmuergen@example.com' - ], - 'Subject' => 'Die Hasen und die Frösche (Microsoft Outlook 00)', - 'text' => 'HasenundFrosche.txt', - 'signed' => [ - 'protocol' => 'application/x-pgp-signature', - 'signed-part-protocol' => 'application/pgp-signature', - 'micalg' => 'pgp-sha256', - 'body' => '9825cba003a7ac85b9a3f3dc9f8423fd' - ], - ]); - } - - public function testSetSignedPartm4008() - { - $handle = fopen($this->messageDir . '/m4008.txt', 'r'); - $message = $this->parser->parse($handle); - fclose($handle); - - $text = 'For the Mighty Meint :)'; - $message->setTextPart($text); - $message->setAsMultipartSigned('pgp-sha256', 'application/pgp-signature'); - - $signableContent = $message->getSignableBody(); - file_put_contents(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sigpart_m4008", $signableContent); - $signature = $this->getSignatureForContent($signableContent); - $message->createSignaturePart($signature); - - $tmpSaved = fopen(dirname(dirname(__DIR__)) . '/' . TEST_OUTPUT_DIR . "/sig_m4008", 'w+'); - $message->save($tmpSaved); - rewind($tmpSaved); - - $this->assertContains($signableContent, stream_get_contents($tmpSaved)); - rewind($tmpSaved); - - $messageWritten = $this->parser->parse($tmpSaved); - fclose($tmpSaved); - $failMessage = 'Failed while parsing saved message for m4008'; - - $testString = $messageWritten->getOriginalMessageStringForSignatureVerification(); - $this->assertEquals($testString, $signableContent); - $this->assertEquals($this->getSignatureForContent($testString), $signature); - - $props = [ - 'From' => [ - 'name' => 'Doug Sauder', - 'email' => 'doug@example.com' - ], - 'To' => [ - 'name' => 'Jürgen Schmürgen', - 'email' => 'schmuergen@example.com' - ], - 'Subject' => 'Die Hasen und die Frösche (Microsoft Outlook 00)', - 'signed' => [ - 'protocol' => 'application/pgp-signature', - 'micalg' => 'pgp-sha256', - 'body' => $signature - ], - ]; - - $this->runEmailTestForMessage($messageWritten, $props, $failMessage); - $this->assertEquals($text, trim($messageWritten->getTextContent())); - } } diff --git a/tests/MailMimeParser/MailMimeParserTest.php b/tests/MailMimeParser/MailMimeParserTest.php index 7b158c94..17b78498 100644 --- a/tests/MailMimeParser/MailMimeParserTest.php +++ b/tests/MailMimeParser/MailMimeParserTest.php @@ -23,7 +23,7 @@ public function testParseFromHandle() { $mmp = new MailMimeParser(); - $handle = fopen('php://memory', 'rw'); + $handle = fopen('php://memory', 'r+'); fwrite($handle, 'This is a test'); rewind($handle); diff --git a/tests/MailMimeParser/Message/MessageFactoryTest.php b/tests/MailMimeParser/Message/MessageFactoryTest.php new file mode 100644 index 00000000..345c898e --- /dev/null +++ b/tests/MailMimeParser/Message/MessageFactoryTest.php @@ -0,0 +1,67 @@ +getMockBuilder('ZBateson\MailMimeParser\Stream\StreamFactory') + ->getMock(); + $mockpsfm = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager') + ->disableOriginalConstructor() + ->getMock(); + $mockpsfmfactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\Factory\PartStreamFilterManagerFactory') + ->disableOriginalConstructor() + ->getMock(); + $mockHeaderFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Header\HeaderFactory') + ->disableOriginalConstructor() + ->setMethods(['newInstance']) + ->getMock(); + $mockFilterFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilterFactory') + ->disableOriginalConstructor() + ->getMock(); + $mockHelperService = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Helper\MessageHelperService') + ->disableOriginalConstructor() + ->getMock(); + + $mockpsfmfactory->method('newInstance') + ->willReturn($mockpsfm); + + $this->messageFactory = new MessageFactory( + $mocksdf, + $mockpsfmfactory, + $mockHeaderFactory, + $mockFilterFactory, + $mockHelperService + ); + } + + public function testNewInstance() + { + $partBuilder = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $part = $this->messageFactory->newInstance( + $partBuilder, + Psr7\stream_for('test') + ); + $this->assertInstanceOf( + '\ZBateson\MailMimeParser\Message', + $part + ); + } +} diff --git a/tests/MailMimeParser/Message/MessageParserTest.php b/tests/MailMimeParser/Message/MessageParserTest.php index dba0191a..f0ee56e4 100644 --- a/tests/MailMimeParser/Message/MessageParserTest.php +++ b/tests/MailMimeParser/Message/MessageParserTest.php @@ -2,9 +2,11 @@ namespace ZBateson\MailMimeParser\Message; use PHPUnit_Framework_TestCase; +use GuzzleHttp\Psr7; +use org\bovigo\vfs\vfsStream; /** - * Description of ParserTest + * MessageParserTest * * @group MessageParser * @group Message @@ -13,66 +15,64 @@ */ class MessageParserTest extends PHPUnit_Framework_TestCase { - protected function getMockedMessage() + protected $partFactoryService; + protected $partBuilderFactory; + protected $partStreamRegistry; + protected $messageFactory; + protected $uuEncodedPartFactory; + protected $mimePartFactory; + protected $vfs; + + public function setUp() { - $message = $this->getMockBuilder('ZBateson\MailMimeParser\Message') + $this->vfs = vfsStream::setup('root'); + + $this->partFactoryService = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\Factory\PartFactoryService') ->disableOriginalConstructor() - ->setMethods([ - 'getObjectId', - 'setRawHeader', - 'getHeader', - 'getHeaderValue', - 'getHeaderParameter', - 'addPart' - ]) ->getMock(); - $message->method('getObjectId')->willReturn('mocked'); - return $message; - } - - protected function getMockedPart() - { - $part = $this->getMockBuilder('ZBateson\MailMimeParser\Message\MimePart') + + $this->partBuilderFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\Factory\PartBuilderFactory') ->disableOriginalConstructor() - ->setMethods(['setRawHeader', 'getHeader', 'getHeaderValue', 'getHeaderParameter']) ->getMock(); - return $part; - } - - protected function getMockedUUEncodedPart() - { - $part = $this->getMockBuilder('ZBateson\MailMimeParser\Message\UUEncodedPart') + + $this->partStreamRegistry = $this->getMockBuilder('ZBateson\MailMimeParser\Stream\PartStreamRegistry') ->disableOriginalConstructor() - ->setMethods(['setRawHeader', 'getHeader', 'getHeaderValue', 'getHeaderParameter']) ->getMock(); - return $part; - } - - protected function getMockedNonMimePart() - { - $part = $this->getMockBuilder('ZBateson\MailMimeParser\Message\NonMimePart') + + $this->messageFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\MessageFactory') + ->disableOriginalConstructor() + ->getMock(); + + $this->uuEncodedPartFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\Factory\UUEncodedPartFactory') + ->disableOriginalConstructor() + ->getMock(); + + $this->mimePartFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\Factory\MimePartFactory') ->disableOriginalConstructor() - ->setMethods(['setRawHeader', 'getHeader', 'getHeaderValue', 'getHeaderParameter']) ->getMock(); - return $part; } - protected function getMockedPartFactory() + protected function getMimePartMock() { - $partFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\MimePartFactory') + return $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\MimePart') ->disableOriginalConstructor() ->getMock(); - return $partFactory; } - protected function getMockedPartStreamRegistry() + protected function getPartBuilderMock($mimeMock = null) { - $partStreamRegistry = $this->getMockBuilder('ZBateson\MailMimeParser\Stream\PartStreamRegistry') + if ($mimeMock === null) { + $mimeMock = $this->getMimePartMock(); + } + $pb = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartBuilder') + ->disableOriginalConstructor() ->getMock(); - return $partStreamRegistry; + $pb->method('createMessagePart') + ->willReturn($mimeMock); + return $pb; } - - protected function callParserWithEmail($emailText, $message, $partFactory, $partStreamRegistry) + + protected function callParserWithEmail($emailText, $messageFactory, $mimePartFactory, $partBuilderFactory, $partStreamRegistry) { $email = fopen('php://memory', 'rw'); fwrite( @@ -80,42 +80,273 @@ protected function callParserWithEmail($emailText, $message, $partFactory, $part $emailText ); rewind($email); - $parser = new MessageParser($message, $partFactory, $partStreamRegistry); + $parser = new MessageParser($messageFactory, $mimePartFactory, $partBuilderFactory, $partStreamRegistry); $parser->parse($email); fclose($email); } - public function testParseSimpleMessage() + public function testParseEmptyMessage() + { + $content = vfsStream::newFile('part')->at($this->vfs); + $content->withContent(''); + $handle = fopen($content->url(), 'r'); + + $pfs = $this->partFactoryService; + $pfs->method('getMessageFactory') + ->willReturn($this->messageFactory); + + $pb = $this->getPartBuilderMock(); + $pb->expects($this->once()) + ->method('setStreamPartStartPos') + ->with(0); + $pb->expects($this->once()) + ->method('canHaveHeaders'); + $pb->expects($this->once()) + ->method('getParent') + ->willReturn(null); + $pb->expects($this->never()) + ->method('addHeader'); + $pb->expects($this->once()) + ->method('isMime') + ->willReturn(false); + $pb->expects($this->once()) + ->method('setStreamPartEndPos') + ->with(0); + + $pbf = $this->partBuilderFactory; + $pbf->method('newPartBuilder') + ->willReturn($pb); + + $mp = new MessageParser($pfs, $pbf, $this->partStreamRegistry); + $message = $mp->parse(Psr7\stream_for($handle)); + $this->assertNotNull($message); + + fclose($handle); + } + + public function testParseSimpleNonMimeMessage() { $email = - "Content-Type: text/html\r\n" - . "Subject: Money owed for services rendered\r\n" + "Subject: Money owed for services rendered\r\n" . "\r\n"; $startPos = strlen($email); $email .= "Dear Albert,\r\n\r\nAfter our wonderful time together, it's unfortunate I know, but I expect payment\r\n" . "for all services hereby rendered.\r\n\r\nYours faithfully,\r\nKandice Waterskyfalls"; $endPos = strlen($email); - $message = $this->getMockedMessage(); - $message->method('getHeaderValue')->willReturn('text/html'); - $message->expects($this->exactly(2)) - ->method('setRawHeader') + $content = vfsStream::newFile('part')->at($this->vfs); + $content->withContent($email); + $handle = fopen($content->url(), 'r'); + + $pfs = $this->partFactoryService; + $pfs->method('getMessageFactory') + ->willReturn($this->messageFactory); + + $pb = $this->getPartBuilderMock(); + $pb->expects($this->once()) + ->method('setStreamPartStartPos') + ->with(0); + $pb->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pb->expects($this->once()) + ->method('addHeader') + ->with('Subject', 'Money owed for services rendered'); + $pb->expects($this->once()) + ->method('getParent') + ->willReturn(null); + $pb->expects($this->once()) + ->method('isMime') + ->willReturn(false); + $pb->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($startPos); + $pb->expects($this->exactly(7)) + ->method('setStreamPartAndContentEndPos') + ->withConsecutive([$this->anything()], [$this->anything()], [$this->anything()], + [$this->anything()], [$this->anything()], [$this->anything()], [$endPos]); + $pb->expects($this->once()) + ->method('setStreamPartEndPos') + ->with($endPos); + + $pbf = $this->partBuilderFactory; + $pbf->method('newPartBuilder') + ->willReturn($pb); + + $mp = new MessageParser($pfs, $pbf, $this->partStreamRegistry); + $message = $mp->parse(Psr7\stream_for($handle)); + $this->assertNotNull($message); + + fclose($handle); + } + + public function testParseMessageWithUUEncodedAttachments() + { + $email = + "Subject: The Diamonds\r\n" + . "To: Cousin Avi\r\n" + . "\r\n"; + $startPos = strlen($email); + $email .= "Aviiiiiiiiii...\r\n\r\n"; + $contentEnd = strlen($email); + $email .= "begin 666 message.txt\r\n" + . 'Listen to me... if the stones are kosher, then I\'ll buy them, won\'t I?' + . "\r\nend\r\n\r\n"; + $endPos = strlen($email); + $startPos2 = $endPos; + $email .= "begin 600 message2.txt\r\n" + . 'No, Tommy. ... It\'s tiptop. It\'s just I\'m not sure about the colour.' + . "\r\nend\r\n"; + $endEmailPos = strlen($email); + + $content = vfsStream::newFile('part')->at($this->vfs); + $content->withContent($email); + $handle = fopen($content->url(), 'r'); + + $pfs = $this->partFactoryService; + $pfs->method('getMessageFactory') + ->willReturn($this->messageFactory); + $pfs->expects($this->exactly(2)) + ->method('getUUEncodedPartFactory') + ->willReturn($this->uuEncodedPartFactory); + + $pbm = $this->getPartBuilderMock(); + $pbm->expects($this->once()) + ->method('setStreamPartStartPos') + ->with(0); + $pbm->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pbm->expects($this->exactly(2)) + ->method('addHeader') ->withConsecutive( - ['Content-Type', 'text/html'], - ['Subject', 'Money owed for services rendered'] + ['Subject', 'The Diamonds'], + ['To', 'Cousin Avi'] ); - - $partFactory = $this->getMockedPartFactory(); - $self = $this; - $partFactory->method('newMimePart')->will($this->returnCallback(function () use ($self) { - return $self->getMockedPart(); - })); - $partStreamRegistry = $this->getMockedPartStreamRegistry(); - $partStreamRegistry->expects($this->once()) - ->method('attachContentPartStreamHandle') - ->with($this->anything(), $this->anything(), $startPos, $endPos); - - $this->callParserWithEmail($email, $message, $partFactory, $partStreamRegistry); + $pbm->expects($this->once()) + ->method('getParent') + ->willReturn(null); + $pbm->expects($this->once()) + ->method('isMime') + ->willReturn(false); + $pbm->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($startPos); + $pbm->expects($this->exactly(2)) + ->method('setStreamPartAndContentEndPos') + ->withConsecutive([$this->anything()], [$contentEnd]); + $pbm->expects($this->once()) + ->method('setStreamPartEndPos') + ->with($endEmailPos); + + $pba1 = $this->getPartBuilderMock(); + $pba1->expects($this->once()) + ->method('setStreamPartStartPos') + ->with($contentEnd); + $pba1->expects($this->exactly(2)) + ->method('setProperty') + ->withConsecutive( + ['mode', '666'], + ['filename', 'message.txt'] + ); + $pba1->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($contentEnd); + $pba1->expects($this->exactly(4)) + ->method('setStreamPartAndContentEndPos') + ->withConsecutive([$this->anything()], [$this->anything()], + [$this->anything()], [$endPos]); + + $pba2 = $this->getPartBuilderMock(); + $pba2->expects($this->once()) + ->method('setStreamPartStartPos') + ->with($startPos2); + $pba2->expects($this->exactly(2)) + ->method('setProperty') + ->withConsecutive( + ['mode', '600'], + ['filename', 'message2.txt'] + ); + $pba2->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($startPos2); + $pba2->expects($this->exactly(3)) + ->method('setStreamPartAndContentEndPos') + ->withConsecutive([$this->anything()], [$this->anything()], + [$endEmailPos]); + + $pbm->expects($this->exactly(2)) + ->method('addChild') + ->withConsecutive([$pba1], [$pba2]); + + $pbf = $this->partBuilderFactory; + $pbf->expects($this->exactly(3)) + ->method('newPartBuilder') + ->willReturnOnConsecutiveCalls( + $pbm, $pba1, $pba2 + ); + + $mp = new MessageParser($pfs, $pbf, $this->partStreamRegistry); + $message = $mp->parse(Psr7\stream_for($handle)); + $this->assertNotNull($message); + + fclose($handle); + } + + public function testParseSimpleMimeMessage() + { + $email = + "Subject: Money owed for services rendered\n" + . "Content-Type: text/html\n" + . "\n"; + $startPos = strlen($email); + $email .= "Dear Albert,\r\n\r\nAfter our wonderful time together, it's unfortunate I know, but I expect payment\r\n" + . "for all services hereby rendered.\r\n\r\nYours faithfully,\r\nKandice Waterskyfalls"; + $endPos = strlen($email); + + $content = vfsStream::newFile('part')->at($this->vfs); + $content->withContent($email); + $handle = fopen($content->url(), 'r'); + + $pfs = $this->partFactoryService; + $pfs->method('getMessageFactory') + ->willReturn($this->messageFactory); + + $pb = $this->getPartBuilderMock(); + $pb->expects($this->once()) + ->method('setStreamPartStartPos') + ->with(0); + $pb->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pb->expects($this->exactly(2)) + ->method('addHeader') + ->withConsecutive( + ['Subject', 'Money owed for services rendered'], + ['Content-Type', 'text/html'] + ); + $pb->expects($this->once()) + ->method('getParent') + ->willReturn(null); + $pb->expects($this->once()) + ->method('isMime') + ->willReturn(true); + $pb->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($startPos); + $pb->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($endPos); + + $pbf = $this->partBuilderFactory; + $pbf->method('newPartBuilder') + ->willReturn($pb); + + $mp = new MessageParser($pfs, $pbf, $this->partStreamRegistry); + $message = $mp->parse(Psr7\stream_for($handle)); + $this->assertNotNull($message); + + fclose($handle); } public function testParseMultipartAlternativeMessage() @@ -127,66 +358,163 @@ public function testParseMultipartAlternativeMessage() . "\r\n"; $messagePartStart = strlen($email); - $email .= - "--balderdash\r\n" - . "Content-Type: text/html\r\n" - . "\r\n"; + $email .= "--balderdash\r\n"; $partOneStart = strlen($email); + $email .= "Content-Type: text/html\r\n" + . "\r\n"; + $partOneContentStart = strlen($email); $email .= "

I'm a little teapot, short and stout. Where is my guiness, where is" . "my draught. I certainly can't rhyme, but no I'm not daft.

\r\n"; $partOneEnd = strlen($email); - $email .= - "--balderdash\r\n" - . "Content-Type: text/plain\r\n" - . "\r\n"; + $email .= "\r\n--balderdash\r\n"; $partTwoStart = strlen($email); + $email .= "Content-Type: text/plain\r\n" + . "\r\n"; + $partTwoContentStart = strlen($email); $email .= "I'm a little teapot, short and stout. Where is my guiness, where is" - . "my draught. I certainly can't rhyme, but no I'm not daft.\r\n"; + . "my draught. I certainly can't rhyme, but no I'm not daft."; $partTwoEnd = strlen($email); - $email .= "--balderdash--\r\n\r\n"; + $email .= "\r\n--balderdash--\r\n\r\n"; + $emailEnd = strlen($email); - $firstPart = $this->getMockedPart(); - $firstPart->expects($this->once()) - ->method('setRawHeader') - ->with('Content-Type', 'text/html'); - $firstPart->method('getHeaderValue') - ->willReturn('text/html'); + $content = vfsStream::newFile('part')->at($this->vfs); + $content->withContent($email); + $handle = fopen($content->url(), 'r'); - $secondPart = $this->getMockedPart(); - $secondPart->expects($this->once()) - ->method('setRawHeader') - ->with('Content-Type', 'text/plain'); - $secondPart->method('getHeaderValue') - ->willReturn('text/plain'); - - $message = $this->getMockedMessage(); - $message->method('getHeaderValue') - ->willReturn('multipart/alternative'); - $message->method('getHeaderParameter') - ->willReturn('balderdash'); - $message->expects($this->exactly(2)) - ->method('addPart') + $pfs = $this->partFactoryService; + $pfs->method('getMessageFactory') + ->willReturn($this->messageFactory); + $pfs->expects($this->exactly(3)) + ->method('getMimePartFactory') + ->willReturn($this->mimePartFactory); + + $pbm = $this->getPartBuilderMock(); + $pbm->expects($this->once()) + ->method('setStreamPartStartPos') + ->with(0); + $pbm->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pbm->expects($this->exactly(2)) + ->method('addHeader') ->withConsecutive( - [$firstPart], - [$secondPart] + ['Content-Type', "multipart/alternative;\r\n boundary=balderdash"], + ['Subject', 'I\'m a tiny little wee teapot'] ); + $pbm->expects($this->once()) + ->method('getParent') + ->willReturn(null); + $pbm->expects($this->once()) + ->method('isMime') + ->willReturn(true); + $pbm->expects($this->once()) + ->method('isMultiPart') + ->willReturn(true); + $pbm->expects($this->once()) + ->method('setEndBoundaryFound') + ->with('--balderdash') + ->willReturn(true); + $pbm->expects($this->exactly(4)) + ->method('isParentBoundaryFound') + ->willReturnOnConsecutiveCalls(false, false, false, true); + $pbm->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($messagePartStart); + $pbm->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($messagePartStart); + + $pba1 = $this->getPartBuilderMock(); + $pba1->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pba1->expects($this->once()) + ->method('addHeader') + ->with('Content-Type', 'text/html'); + $pba1->expects($this->once()) + ->method('getParent') + ->willReturn($pbm); + $pba1->expects($this->exactly(3)) + ->method('setEndBoundaryFound') + ->willReturnMap([ + [$this->anything(), false], + [$this->anything(), false], + ['--balderdash', true] + ]); + $pba1->expects($this->once()) + ->method('setStreamPartStartPos') + ->with($partOneStart); + $pba1->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($partOneContentStart); + $pba1->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($partOneEnd); - $partFactory = $this->getMockedPartFactory(); - $partFactory->method('newMimePart')->will($this->onConsecutiveCalls($firstPart, $secondPart, $this->getMockedPart())); - $partStreamRegistry = $this->getMockedPartStreamRegistry(); - $partStreamRegistry->expects($this->exactly(3)) - ->method('attachContentPartStreamHandle') - ->withConsecutive( - [$message, $message, $messagePartStart, $messagePartStart], - [$firstPart, $message, $partOneStart, $partOneEnd], - [$secondPart, $message, $partTwoStart, $partTwoEnd] + $pba2 = $this->getPartBuilderMock(); + $pba2->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pba2->expects($this->once()) + ->method('addHeader') + ->with('Content-Type', 'text/plain'); + $pba2->expects($this->once()) + ->method('getParent') + ->willReturn($pbm); + $pba2->expects($this->exactly(2)) + ->method('setEndBoundaryFound') + ->willReturnMap([ + [$this->anything(), false], + ['--balderdash--', true] + ]); + $pba2->expects($this->once()) + ->method('setStreamPartStartPos') + ->with($partTwoStart); + $pba2->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($partTwoContentStart); + $pba2->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($partTwoEnd); + + $pba3 = $this->getPartBuilderMock(); + $pba3->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(false); + $pba3->expects($this->once()) + ->method('getParent') + ->willReturn($pbm); + $pba3->expects($this->once()) + ->method('setEndBoundaryFound') + ->with('') + ->willReturn(false); + $pba3->expects($this->once()) + ->method('setEof'); + $pba3->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($emailEnd); + + $pbm->expects($this->exactly(3)) + ->method('addChild') + ->withConsecutive([$pba1], [$pba2], [$pba3]); + + $pbf = $this->partBuilderFactory; + $pbf->expects($this->exactly(4)) + ->method('newPartBuilder') + ->willReturnOnConsecutiveCalls( + $pbm, $pba1, $pba2, $pba3 ); - $this->callParserWithEmail($email, $message, $partFactory, $partStreamRegistry); + + $mp = new MessageParser($pfs, $pbf, $this->partStreamRegistry); + $message = $mp->parse(Psr7\stream_for($handle)); + $this->assertNotNull($message); + + fclose($handle); } - public function testParseMultipartMixedMessage() + public function testParseMultipartMixedWithAlternativeMessage() { $email = "Content-Type: multipart/mixed; boundary=balderdash\r\n" @@ -194,194 +522,310 @@ public function testParseMultipartMixedMessage() . "\r\n"; $messagePartStart = strlen($email); - $email .= "This existed for nought - hidden from view\r\n"; + $email .= "This existed for nought - hidden from view"; $messagePartEnd = strlen($email); - $email .= - "--balderdash\r\n" - . "Content-Type: multipart/alternative; boundary=gobbledygook\r\n" + $email .= "\r\n--balderdash\r\n"; + $altPartStart = strlen($email); + $email .= "Content-Type: multipart/alternative; boundary=gobbledygook\r\n" . "\r\n"; - $altPartStart = strlen($email); - $email .= "A line to fool the senses was created... and it was this line\r\n"; - $altPartEnd = strlen($email); + $altPartContentStart = strlen($email); + $email .= "A line to fool the senses was created... and it was this line"; + $altPartContentEnd = strlen($email); - $email .= - "--gobbledygook\r\n" - . "Content-Type: text/html\r\n" - . "\r\n"; + $email .= "\r\n--gobbledygook\r\n"; $partOneStart = strlen($email); + $email .= "Content-Type: text/html\r\n" + . "\r\n"; + $partOneContentStart = strlen($email); $email .= "

There once was a man, who was both man and mouse. He thought himself" . "pretty, but was really - well - as ugly as you can imagine a creature" - . "that is part man and part mouse.

\r\n"; + . "that is part man and part mouse.

"; $partOneEnd = strlen($email); - $email .= - "--gobbledygook\r\n" - . "Content-Type: text/plain\r\n" - . "\r\n"; + $email .= "\r\n--gobbledygook\r\n"; $partTwoStart = strlen($email); + $email .= "Content-Type: text/plain\r\n" + . "\r\n"; + $partTwoContentStart = strlen($email); $email .= "There once was a man, who was both man and mouse. He thought himself" . "pretty, but was really - well - as ugly as you can imagine a creature" - . "that is part man and part mouse.\r\n"; + . "that is part man and part mouse."; $partTwoEnd = strlen($email); - $email .= - "--gobbledygook--\r\n" - . "--balderdash\r\n" - . "Content-Type: text/html\r\n" - . "\r\n"; + $email .= "\r\n--gobbledygook--"; + $email .= "\r\n--balderdash\r\n"; $partThreeStart = strlen($email); - $email .= "

He wandered through the lands, and shook fancy hands.

\r\n"; - $partThreeEnd = strlen($email); - $email .= - "--balderdash\r\n" - . "Content-Type: text/plain\r\n" + $email .= "Content-Type: text/html\r\n" . "\r\n"; + $partThreeContentStart = strlen($email); + $email .= "

He wandered through the lands, and shook fancy hands.

"; + $partThreeEnd = strlen($email); + $email .= "\r\n--balderdash\r\n"; $partFourStart = strlen($email); - $email .= " (^^) \r\n"; + $email .= "\r\n"; + $partFourContentStart = strlen($email); + $email .= " (^^) "; $partFourEnd = strlen($email); - $email .= "--balderdash--\r\n"; + $email .= "\r\n--balderdash--\r\n"; + $emailEnd = strlen($email); + + $content = vfsStream::newFile('part')->at($this->vfs); + $content->withContent($email); + $handle = fopen($content->url(), 'r'); - $firstPart = $this->getMockedPart(); - $firstPart->expects($this->once()) - ->method('setRawHeader') - ->with('Content-Type', 'multipart/alternative; boundary=gobbledygook'); - $firstPart->method('getHeaderValue') - ->willReturn('multipart/alternative'); - $firstPart->method('getHeaderParameter') - ->willReturn('gobbledygook'); - - $secondPart = $this->getMockedPart(); - $secondPart->expects($this->once()) - ->method('setRawHeader') - ->with('Content-Type', 'text/html'); - $secondPart->method('getHeaderValue') - ->willReturn('text/html'); + $pfs = $this->partFactoryService; + $pfs->method('getMessageFactory') + ->willReturn($this->messageFactory); + $pfs->expects($this->exactly(7)) + ->method('getMimePartFactory') + ->willReturn($this->mimePartFactory); - $thirdPart = $this->getMockedPart(); - $thirdPart->expects($this->once()) - ->method('setRawHeader') - ->with('Content-Type', 'text/plain'); - $thirdPart->method('getHeaderValue') - ->willReturn('text/plain'); + $pbm = $this->getPartBuilderMock(); + $pbm->expects($this->once()) + ->method('setStreamPartStartPos') + ->with(0); + $pbm->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pbm->expects($this->exactly(2)) + ->method('addHeader') + ->withConsecutive( + ['Content-Type', "multipart/mixed; boundary=balderdash"], + ['Subject', 'Of mice and men'] + ); + $pbm->expects($this->once()) + ->method('isMime') + ->willReturn(true); + $pbm->expects($this->once()) + ->method('isMultiPart') + ->willReturn(true); + $pbm->expects($this->exactly(2)) + ->method('setEndBoundaryFound') + ->withConsecutive( + ['This existed for nought - hidden from view'], + ['--balderdash'] + ) + ->willReturnOnConsecutiveCalls(false, true); + $pbm->expects($this->exactly(5)) + ->method('isParentBoundaryFound') + ->willReturnOnConsecutiveCalls(false, false, false, false, true); + $pbm->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($messagePartStart); + $pbm->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($messagePartEnd); - $fourthPart = $this->getMockedPart(); - $fourthPart->expects($this->once()) - ->method('setRawHeader') + $pbAlt = $this->getPartBuilderMock(); + $pbAlt->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pbAlt->expects($this->once()) + ->method('getParent') + ->willReturn($pbm); + $pbAlt->expects($this->once()) + ->method('addHeader') + ->with('Content-Type', 'multipart/alternative; boundary=gobbledygook'); + $pbAlt->expects($this->once()) + ->method('isMultiPart') + ->willReturn(true); + $pbAlt->expects($this->exactly(2)) + ->method('setEndBoundaryFound') + ->withConsecutive( + ['A line to fool the senses was created... and it was this line'], + ['--gobbledygook'] + ) + ->willReturnOnConsecutiveCalls(false, true); + $pbAlt->expects($this->exactly(4)) + ->method('isParentBoundaryFound') + ->willReturnOnConsecutiveCalls(false, false, false, true); + $pbAlt->expects($this->once()) + ->method('setStreamPartStartPos') + ->with($altPartStart); + $pbAlt->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($altPartContentStart); + $pbAlt->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($altPartContentEnd); + + $pba1 = $this->getPartBuilderMock(); + $pba1->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pba1->expects($this->once()) + ->method('getParent') + ->willReturn($pbAlt); + $pba1->expects($this->once()) + ->method('addHeader') ->with('Content-Type', 'text/html'); - $fourthPart->method('getHeaderValue') - ->willReturn('text/html'); + $pba1->expects($this->exactly(2)) + ->method('setEndBoundaryFound') + ->withConsecutive( + ["

There once was a man, who was both man and mouse. He thought himself" + . "pretty, but was really - well - as ugly as you can imagine a creature" + . "that is part man and part mouse.

"], + ['--gobbledygook'] + ) + ->willReturnOnConsecutiveCalls(false, true); + $pba1->expects($this->once()) + ->method('isMultiPart') + ->willReturn(false); + $pba1->expects($this->once()) + ->method('setStreamPartStartPos') + ->with($partOneStart); + $pba1->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($partOneContentStart); + $pba1->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($partOneEnd); - $fifthPart = $this->getMockedPart(); - $fifthPart->expects($this->once()) - ->method('setRawHeader') + $pba2 = $this->getPartBuilderMock(); + $pba2->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pba2->expects($this->once()) + ->method('getParent') + ->willReturn($pbAlt); + $pba2->expects($this->once()) + ->method('addHeader') ->with('Content-Type', 'text/plain'); - $fifthPart->method('getHeaderValue') - ->willReturn('text/plain'); - - $message = $this->getMockedMessage(); - $message->method('getHeaderValue') - ->willReturn('multipart/mixed'); - $message->method('getHeaderParameter') - ->willReturn('balderdash'); - - $message->expects($this->exactly(3)) - ->method('addPart'); - - $partFactory = $this->getMockedPartFactory(); - $partFactory->method('newMimePart')->will($this->onConsecutiveCalls( - $firstPart, - $secondPart, - $thirdPart, - $fourthPart, - $fifthPart, - $this->getMockedPart() - )); - $partStreamRegistry = $this->getMockedPartStreamRegistry(); - $partStreamRegistry->expects($this->exactly(6)) - ->method('attachContentPartStreamHandle') + $pba2->expects($this->exactly(2)) + ->method('setEndBoundaryFound') ->withConsecutive( - [$message, $message, $messagePartStart, $messagePartEnd], - [$this->anything(), $message, $altPartStart, $altPartEnd], - [$this->anything(), $message, $partOneStart, $partOneEnd], - [$thirdPart, $message, $partTwoStart, $partTwoEnd], - [$fourthPart, $message, $partThreeStart, $partThreeEnd], - [$fifthPart, $message, $partFourStart, $partFourEnd] - ); - $this->callParserWithEmail($email, $message, $partFactory, $partStreamRegistry); - } - - public function testParseUUEncodedMessage() - { - $email = - "Subject: The Diamonds\r\n" - . "To: Cousin Avi\r\n" - . "\r\n"; - $startPos = strlen($email); - $messageText = 'Listen to me... if the stones are kosher, then I\'ll buy them, won\'t I?'; - $email .= "begin 664 message.txt\r\n" - . convert_uuencode($messageText) - . "\r\nend\r\n\r\n"; - $endPos = strlen($email); - $startPos2 = $endPos; - $email .= "begin 664 message2.txt\r\n" - . convert_uuencode('No, Tommy. ... It\'s tiptop. It\'s just I\'m not sure about the colour.') - . "\r\nend\r\n"; - $endPos2 = strlen($email); + ["There once was a man, who was both man and mouse. He thought himself" + . "pretty, but was really - well - as ugly as you can imagine a creature" + . "that is part man and part mouse."], + ['--gobbledygook--'] + ) + ->willReturnOnConsecutiveCalls(false, true); + $pba2->expects($this->once()) + ->method('isMultiPart') + ->willReturn(false); + $pba2->expects($this->once()) + ->method('setStreamPartStartPos') + ->with($partTwoStart); + $pba2->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($partTwoContentStart); + $pba2->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($partTwoEnd); - $message = $this->getMockedMessage(); - $message->expects($this->exactly(2)) - ->method('setRawHeader') + $pba3 = $this->getPartBuilderMock(); + $pba3->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(false); + $pba3->expects($this->once()) + ->method('getParent') + ->willReturn($pbAlt); + $pba3->expects($this->once()) + ->method('isMultiPart') + ->willReturn(false); + $pba3->expects($this->once()) + ->method('setEndBoundaryFound') + ->with('--balderdash') + ->willReturn(true); + + $pba4 = $this->getPartBuilderMock(); + $pba4->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pba4->expects($this->once()) + ->method('getParent') + ->willReturn($pbm); + $pba4->expects($this->once()) + ->method('addHeader') + ->with('Content-Type', 'text/html'); + $pba4->expects($this->exactly(2)) + ->method('setEndBoundaryFound') ->withConsecutive( - ['Subject', 'The Diamonds'], - ['To', 'Cousin Avi'] - ); - - $partFactory = $this->getMockedPartFactory(); - $self = $this; - $partFactory->method('newUUEncodedPart')->will($this->returnCallback(function () use ($self) { - return $self->getMockedUUEncodedPart(); - })); - $partStreamRegistry = $this->getMockedPartStreamRegistry(); - $partStreamRegistry->method('attachContentPartStreamHandle') + ['

He wandered through the lands, and shook fancy hands.

'], + ['--balderdash'] + ) + ->willReturnOnConsecutiveCalls(false, true); + $pba4->expects($this->once()) + ->method('isMultiPart') + ->willReturn(false); + $pba4->expects($this->once()) + ->method('setStreamPartStartPos') + ->with($partThreeStart); + $pba4->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($partThreeContentStart); + $pba4->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($partThreeEnd); + + $pba5 = $this->getPartBuilderMock(); + $pba5->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(true); + $pba5->expects($this->once()) + ->method('getParent') + ->willReturn($pbm); + $pba5->expects($this->once()) + ->method('isMultiPart') + ->willReturn(false); + $pba5->expects($this->never()) + ->method('addHeader'); + $pba5->expects($this->exactly(2)) + ->method('setEndBoundaryFound') ->withConsecutive( - [$this->anything(), $this->anything(), $startPos, $endPos], - [$this->anything(), $this->anything(), $startPos2, $endPos2] - ); + [' (^^) '], + ['--balderdash--'] + ) + ->willReturnOnConsecutiveCalls(false, true); + $pba5->expects($this->once()) + ->method('isMultiPart') + ->willReturn(false); + $pba5->expects($this->once()) + ->method('setStreamPartStartPos') + ->with($partFourStart); + $pba5->expects($this->once()) + ->method('setStreamContentStartPos') + ->with($partFourContentStart); + $pba5->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($partFourEnd); - $this->callParserWithEmail($email, $message, $partFactory, $partStreamRegistry); - } - - public function testParseNonMimeMessage() - { - $email = - "Subject: The Diamonds\r\n" - . "To: Cousin Avi\r\n" - . "\r\n"; - $startPos = strlen($email); - $messageText = 'Listen to me... if the stones are kosher, then I\'ll buy them, won\'t I?'; - $email .= $messageText . "\r\n"; - $endPos = strlen($email); + $pba6 = $this->getPartBuilderMock(); + $pba6->expects($this->once()) + ->method('canHaveHeaders') + ->willReturn(false); + $pba6->expects($this->once()) + ->method('getParent') + ->willReturn($pbm); + $pba6->expects($this->once()) + ->method('setStreamPartAndContentEndPos') + ->with($emailEnd); + $pba6->expects($this->once()) + ->method('setEof'); + // no extra trailling characters + $pba6->expects($this->never()) + ->method('setEndBoundaryFound'); - $message = $this->getMockedMessage(); - $message->expects($this->exactly(2)) - ->method('setRawHeader') - ->withConsecutive( - ['Subject', 'The Diamonds'], - ['To', 'Cousin Avi'] + $pbm->expects($this->any()) + ->method('addChild') + ->withConsecutive([$pbAlt], [$pba4], [$pba5], [$pba6]); + $pbAlt->expects($this->exactly(3)) + ->method('addChild') + ->withConsecutive([$pba1], [$pba2], [$pba3]); + + $pbf = $this->partBuilderFactory; + $pbf->expects($this->exactly(8)) + ->method('newPartBuilder') + ->willReturnOnConsecutiveCalls( + $pbm, $pbAlt, $pba1, $pba2, $pba3, $pba4, $pba5, $pba6 ); - - $partFactory = $this->getMockedPartFactory(); - $self = $this; - $partFactory->method('newNonMimePart')->will($this->returnCallback(function () use ($self) { - return $self->getMockedNonMimePart(); - })); - $partStreamRegistry = $this->getMockedPartStreamRegistry(); - $partStreamRegistry->expects($this->once()) - ->method('attachContentPartStreamHandle') - ->with($this->anything(), $this->anything(), $startPos, $endPos); - - $this->callParserWithEmail($email, $message, $partFactory, $partStreamRegistry); + + $mp = new MessageParser($pfs, $pbf, $this->partStreamRegistry); + $message = $mp->parse(Psr7\stream_for($handle)); + $this->assertNotNull($message); + + fclose($handle); } } diff --git a/tests/MailMimeParser/Message/MimePartFactoryTest.php b/tests/MailMimeParser/Message/MimePartFactoryTest.php deleted file mode 100644 index 799c35f4..00000000 --- a/tests/MailMimeParser/Message/MimePartFactoryTest.php +++ /dev/null @@ -1,55 +0,0 @@ -getMockBuilder('ZBateson\MailMimeParser\Header\HeaderFactory') - ->disableOriginalConstructor() - ->getMock(); - $messageWriterService = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Writer\MessageWriterService') - ->disableOriginalConstructor() - ->setMethods(['getMessageWriter']) - ->getMock(); - $messageWriterService->method('getMessagePartWriter')->willReturn( - $this->getMockBuilder('ZBateson\MailMimeParser\Message\Writer\MimePartWriter') - ->disableOriginalConstructor() - ->getMock() - ); - $this->mimePartFactory = new MimePartFactory($headerFactory, $messageWriterService); - } - - public function testNewMimePart() - { - $part = $this->mimePartFactory->newMimePart(); - $this->assertNotNull($part); - $this->assertInstanceOf('\ZBateson\MailMimeParser\Message\MimePart', $part); - } - - public function testNewNonMimePart() - { - $part = $this->mimePartFactory->newNonMimePart(); - $this->assertNotNull($part); - $this->assertInstanceOf('\ZBateson\MailMimeParser\Message\NonMimePart', $part); - } - - public function testNewUUEncodedPart() - { - $part = $this->mimePartFactory->newUUEncodedPart(066, 'test'); - $this->assertNotNull($part); - $this->assertInstanceOf('\ZBateson\MailMimeParser\Message\UUEncodedPart', $part); - } -} diff --git a/tests/MailMimeParser/Message/MimePartTest.php b/tests/MailMimeParser/Message/MimePartTest.php deleted file mode 100644 index fd5ad59a..00000000 --- a/tests/MailMimeParser/Message/MimePartTest.php +++ /dev/null @@ -1,304 +0,0 @@ -mockHeaderFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Header\HeaderFactory') - ->disableOriginalConstructor() - ->setMethods(['newInstance']) - ->getMock(); - $this->mockMessageWriter = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Writer\MimePartWriter') - ->disableOriginalConstructor() - ->getMock(); - } - - protected function getMockedParameterHeader($name, $value, $parameterValue = null) - { - $header = $this->getMockBuilder('ZBateson\MailMimeParser\Header\ParameterHeader') - ->disableOriginalConstructor() - ->setMethods(['getValue', 'getName', 'getValueFor', 'hasParameter']) - ->getMock(); - $header->method('getName')->willReturn($name); - $header->method('getValue')->willReturn($value); - $header->method('getValueFor')->willReturn($parameterValue); - $header->method('hasParameter')->willReturn(true); - return $header; - } - - protected function createNewMimePart() - { - return new MimePart($this->mockHeaderFactory, $this->mockMessageWriter); - } - - protected function createParentAndChild() - { - $hf = $this->mockHeaderFactory; - $header = $this->getMockedParameterHeader('Content-Type', 'text/plain'); - $header2 = $this->getMockedParameterHeader('Content-Type', 'multipart/relative'); - $hf->expects($this->exactly(2)) - ->method('newInstance') - ->withConsecutive( - [$header->getName(), $header->getValue()], - [$header2->getName(), $header2->getValue()] - ) - ->willReturnOnConsecutiveCalls($header, $header2); - - $child = $this->createNewMimePart(); - $parent = $this->createNewMimePart(); - - $child->setRawHeader($header->getName(), $header->getValue()); - $parent->setRawHeader($header2->getName(), $header2->getValue()); - - $parent->addPart($child); - return [ $parent, $child ]; - } - - public function testAttachContentResourceHandle() - { - $part = $this->createNewMimePart(); - $res = fopen('php://memory', 'rw'); - $part->attachContentResourceHandle($res); - $this->assertSame($res, $part->getContentResourceHandle()); - } - - public function testHasContent() - { - $part = $this->createNewMimePart(); - - $this->assertFalse($part->hasContent()); - $res = fopen('php://memory', 'rw'); - $part->attachContentResourceHandle($res); - $this->assertTrue($part->hasContent()); - } - - public function testSetGetAndReadContent() - { - $part = $this->createNewMimePart(); - $content = 'Kilgore Trout was a fella of the highest quality'; - $part->setContent($content); - $this->assertEquals($content, $part->getContent()); - - $res = $part->getContentResourceHandle(); - $this->assertEquals($content, stream_get_contents($res)); - - // read again to ensure call to getContentResourceHandle rewinds it - $res = $part->getContentResourceHandle(); - $this->assertEquals($content, stream_get_contents($res)); - } - - public function testSetRawHeaderAndRemoveHeader() - { - $hf = $this->mockHeaderFactory; - $firstHeader = $this->getMockedParameterHeader('First-Header', 'Value'); - $secondHeader = $this->getMockedParameterHeader('Second-Header', 'Second Value'); - - $hf->expects($this->exactly(2)) - ->method('newInstance') - ->withConsecutive( - [$firstHeader->getName(), $firstHeader->getValue()], - [$secondHeader->getName(), $secondHeader->getValue()] - ) - ->willReturnOnConsecutiveCalls($firstHeader, $secondHeader); - - $part = $this->createNewMimePart(); - $part->setRawHeader($firstHeader->getName(), $firstHeader->getValue()); - $part->setRawHeader($secondHeader->getName(), $secondHeader->getValue()); - $this->assertSame($firstHeader, $part->getHeader($firstHeader->getName())); - $this->assertSame($secondHeader, $part->getHeader($secondHeader->getName())); - $this->assertEquals($firstHeader->getValue(), $part->getHeaderValue($firstHeader->getName())); - $this->assertEquals($secondHeader->getValue(), $part->getHeaderValue($secondHeader->getName())); - $this->assertCount(2, $part->getHeaders()); - $this->assertEquals(['first-header' => $firstHeader, 'second-header' => $secondHeader], $part->getHeaders()); - $part->removeHeader('FIRST-header'); - $this->assertCount(1, $part->getHeaders()); - $this->assertNull($part->getHeader($firstHeader->getName())); - $this->assertNull($part->getHeaderValue($firstHeader->getName())); - $this->assertEquals(['second-header' => $secondHeader], $part->getHeaders()); - } - - public function testHeaderCaseInsensitive() - { - $hf = $this->mockHeaderFactory; - $firstHeader = $this->getMockedParameterHeader('First-Header', 'Value'); - $secondHeader = $this->getMockedParameterHeader('Second-Header', 'Second Value'); - $thirdHeader = $this->getMockedParameterHeader('FIRST-header', 'Third Value'); - - $hf->expects($this->exactly(3)) - ->method('newInstance') - ->withConsecutive( - [$firstHeader->getName(), $firstHeader->getValue()], - [$secondHeader->getName(), $secondHeader->getValue()], - [$thirdHeader->getName(), $thirdHeader->getValue()] - ) - ->willReturnOnConsecutiveCalls($firstHeader, $secondHeader, $thirdHeader); - - $part = $this->createNewMimePart(); - $part->setRawHeader($firstHeader->getName(), $firstHeader->getValue()); - $part->setRawHeader($secondHeader->getName(), $secondHeader->getValue()); - $part->setRawHeader($thirdHeader->getName(), $thirdHeader->getValue()); - - $this->assertSame($thirdHeader, $part->getHeader('first-header')); - $this->assertSame($secondHeader, $part->getHeader('second-header')); - } - - public function testParent() - { - $part = $this->createNewMimePart(); - $parent = $this->createNewMimePart(); - $part->setParent($parent); - $this->assertSame($parent, $part->getParent()); - } - - public function testGetHeaderParameter() - { - $hf = $this->mockHeaderFactory; - $header = $this->getMockedParameterHeader('First-Header', 'Value', 'param-value'); - $hf->expects($this->exactly(1)) - ->method('newInstance') - ->withConsecutive( - [$header->getName(), $header->getValue()] - ) - ->willReturnOnConsecutiveCalls($header); - $part = $this->createNewMimePart(); - $part->setRawHeader($header->getName(), $header->getValue()); - - $this->assertEquals('param-value', $part->getHeaderParameter('first-header', 'param')); - } - - public function testGetUnsetHeader() - { - $part = $this->createNewMimePart(); - $this->assertNull($part->getHeader('Nothing')); - $this->assertNull($part->getHeaderValue('Nothing')); - $this->assertEquals('Default', $part->getHeaderValue('Nothing', 'Default')); - } - - public function testGetUnsetHeaderParameter() - { - $part = $this->createNewMimePart(); - $this->assertNull($part->getHeaderParameter('Nothing', 'Non-Existent')); - $this->assertEquals('Default', $part->getHeaderParameter('Nothing', 'Non-Existent', 'Default')); - } - - public function testIsMultiPart() - { - list($parent, $child) = $this->createParentAndChild(); - $this->assertTrue($parent->isMultiPart()); - $this->assertFalse($child->isMultiPart()); - } - - public function testIsTextPart() - { - list($parent, $child) = $this->createParentAndChild(); - $this->assertFalse($parent->isTextPart()); - $this->assertTrue($child->isTextPart()); - } - - public function testAddAndGetPart() - { - $first = $this->createNewMimePart(); - $second = $this->createNewMimePart(); - $third = $this->createNewMimePart(); - $parent = $this->createNewMimePart(); - - $parent->addPart($first); - $parent->addPart($second); - $second->addPart($third); - - $this->assertSame($parent, $first->getParent()); - $this->assertSame($parent, $second->getParent()); - $this->assertSame($second, $third->getParent()); - - $this->assertEquals(4, $parent->getPartCount()); - $this->assertSame($parent, $parent->getPart(0)); - $this->assertSame($first, $parent->getPart(1)); - $this->assertSame($second, $parent->getPart(2)); - $this->assertSame($third, $parent->getPart(3)); - - $this->assertEquals( - [$parent, $first, $second, $third], - $parent->getAllParts() - ); - } - - public function testAddAndGetFilteredParts() - { - list($parent, $child) = $this->createParentAndChild(); - - $filter = new PartFilter(['textpart' => PartFilter::FILTER_INCLUDE]); - $this->assertEquals(1, $parent->getPartCount($filter)); - $this->assertSame($child, $parent->getPart(0, $filter)); - - $this->assertEquals( - [$child], - $parent->getAllParts($filter) - ); - } - - public function testAddAndGetChildParts() - { - $first = $this->createNewMimePart(); - $second = $this->createNewMimePart(); - $third = $this->createNewMimePart(); - $parent = $this->createNewMimePart(); - - $parent->addPart($first); - $parent->addPart($second); - $second->addPart($third); - - $this->assertSame($parent, $first->getParent()); - $this->assertSame($parent, $second->getParent()); - $this->assertSame($second, $third->getParent()); - - $this->assertEquals(2, $parent->getChildCount()); - $this->assertSame($first, $parent->getChild(0)); - $this->assertSame($second, $parent->getChild(1)); - - $this->assertEquals( - [$first, $second], - $parent->getChildParts() - ); - } - - public function testAddAndGetFilteredChildParts() - { - list($parent, $child) = $this->createParentAndChild(); - - $filter = new PartFilter(['textpart' => PartFilter::FILTER_INCLUDE]); - $this->assertEquals(1, $parent->getChildCount($filter)); - $this->assertSame($child, $parent->getChild(0, $filter)); - - $this->assertEquals( - [$child], - $parent->getChildParts($filter) - ); - } - - public function testAddAndGetPartsByMimeType() - { - list($parent, $child) = $this->createParentAndChild(); - - $this->assertEquals(1, $parent->getCountOfPartsByMimeType('text/plain')); - $this->assertSame($child, $parent->getPartByMimeType('text/plain', 0)); - - $this->assertEquals( - [$child], - $parent->getAllPartsByMimeType('text/plain') - ); - } -} diff --git a/tests/MailMimeParser/Message/NonMimePartTest.php b/tests/MailMimeParser/Message/NonMimePartTest.php deleted file mode 100644 index f3f903fc..00000000 --- a/tests/MailMimeParser/Message/NonMimePartTest.php +++ /dev/null @@ -1,34 +0,0 @@ -getMockBuilder('ZBateson\MailMimeParser\Message\Writer\MimePartWriter') - ->disableOriginalConstructor() - ->getMock(); - - $part = new NonMimePart($hf, $pw); - $this->assertNotNull($part); - $this->assertEquals('text/plain', $part->getHeaderValue('Content-Type')); - } -} diff --git a/tests/MailMimeParser/Message/Part/Factory/MimePartFactoryTest.php b/tests/MailMimeParser/Message/Part/Factory/MimePartFactoryTest.php new file mode 100644 index 00000000..b2443f86 --- /dev/null +++ b/tests/MailMimeParser/Message/Part/Factory/MimePartFactoryTest.php @@ -0,0 +1,62 @@ +getMockBuilder('ZBateson\MailMimeParser\Stream\StreamFactory') + ->getMock(); + $mocksdf->expects($this->any()) + ->method('getLimitedPartStream') + ->willReturn(Psr7\stream_for('test')); + $psfmFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\Factory\PartStreamFilterManagerFactory') + ->disableOriginalConstructor() + ->getMock(); + $psfm = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager') + ->disableOriginalConstructor() + ->getMock(); + $psfmFactory + ->method('newInstance') + ->willReturn($psfm); + + $mockHeaderFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Header\HeaderFactory') + ->disableOriginalConstructor() + ->getMock(); + $mockFilterFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilterFactory') + ->disableOriginalConstructor() + ->getMock(); + $this->mimePartFactory = new MimePartFactory($mocksdf, $psfmFactory, $mockHeaderFactory, $mockFilterFactory); + } + + public function testNewInstance() + { + $partBuilder = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $part = $this->mimePartFactory->newInstance( + $partBuilder, + Psr7\stream_for('test') + ); + $this->assertInstanceOf( + '\ZBateson\MailMimeParser\Message\Part\MimePart', + $part + ); + } +} diff --git a/tests/MailMimeParser/Message/Part/Factory/NonMimePartFactoryTest.php b/tests/MailMimeParser/Message/Part/Factory/NonMimePartFactoryTest.php new file mode 100644 index 00000000..556f61be --- /dev/null +++ b/tests/MailMimeParser/Message/Part/Factory/NonMimePartFactoryTest.php @@ -0,0 +1,55 @@ +getMockBuilder('ZBateson\MailMimeParser\Stream\StreamFactory') + ->getMock(); + $mocksdf->expects($this->any()) + ->method('getLimitedPartStream') + ->willReturn(Psr7\stream_for('test')); + $psfmFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\Factory\PartStreamFilterManagerFactory') + ->disableOriginalConstructor() + ->getMock(); + $psfm = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager') + ->disableOriginalConstructor() + ->getMock(); + $psfmFactory + ->method('newInstance') + ->willReturn($psfm); + + $this->nonMimePartFactory = new NonMimePartFactory($mocksdf, $psfmFactory); + } + + public function testNewInstance() + { + $partBuilder = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $part = $this->nonMimePartFactory->newInstance( + $partBuilder, + Psr7\stream_for('test') + ); + $this->assertInstanceOf( + '\ZBateson\MailMimeParser\Message\Part\NonMimePart', + $part + ); + } +} diff --git a/tests/MailMimeParser/Message/Part/Factory/PartBuilderFactoryTest.php b/tests/MailMimeParser/Message/Part/Factory/PartBuilderFactoryTest.php new file mode 100644 index 00000000..915dac16 --- /dev/null +++ b/tests/MailMimeParser/Message/Part/Factory/PartBuilderFactoryTest.php @@ -0,0 +1,39 @@ +getMockBuilder('ZBateson\MailMimeParser\Header\HeaderFactory') + ->disableOriginalConstructor() + ->setMethods(['newInstance']) + ->getMock(); + $this->partBuilderFactory = new PartBuilderFactory($mockHeaderFactory, 'amazon'); + } + + public function testNewInstance() + { + $mockMessagePartFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\Factory\MessagePartFactory') + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $partBuilder = $this->partBuilderFactory->newPartBuilder($mockMessagePartFactory); + $this->assertInstanceOf( + '\ZBateson\MailMimeParser\Message\Part\PartBuilder', + $partBuilder + ); + } +} diff --git a/tests/MailMimeParser/Message/Part/Factory/PartFactoryServiceTest.php b/tests/MailMimeParser/Message/Part/Factory/PartFactoryServiceTest.php new file mode 100644 index 00000000..6326f2b0 --- /dev/null +++ b/tests/MailMimeParser/Message/Part/Factory/PartFactoryServiceTest.php @@ -0,0 +1,39 @@ +partFactoryService = $di->getPartFactoryService(); + } + + public function testInstance() + { + $messageFactory = $this->partFactoryService->getMessageFactory(); + $this->assertInstanceOf('ZBateson\MailMimeParser\Message\MessageFactory', $messageFactory); + + $mimePartFactory = $this->partFactoryService->getMimePartFactory(); + $this->assertInstanceOf('ZBateson\MailMimeParser\Message\Part\Factory\MimePartFactory', $mimePartFactory); + + $nonMimePartFactory = $this->partFactoryService->getNonMimePartFactory(); + $this->assertInstanceOf('ZBateson\MailMimeParser\Message\Part\Factory\NonMimePartFactory', $nonMimePartFactory); + + $uuEncodedPartFactory = $this->partFactoryService->getUUEncodedPartFactory(); + $this->assertInstanceOf('ZBateson\MailMimeParser\Message\Part\Factory\UUEncodedPartFactory', $uuEncodedPartFactory); + } +} diff --git a/tests/MailMimeParser/Message/Part/Factory/PartStreamFilterManagerFactoryTest.php b/tests/MailMimeParser/Message/Part/Factory/PartStreamFilterManagerFactoryTest.php new file mode 100644 index 00000000..beccbc62 --- /dev/null +++ b/tests/MailMimeParser/Message/Part/Factory/PartStreamFilterManagerFactoryTest.php @@ -0,0 +1,35 @@ +getMockBuilder('ZBateson\MailMimeParser\Stream\StreamFactory') + ->getMock(); + $this->partStreamFilterManagerFactory = new PartStreamFilterManagerFactory( + $mocksdf + ); + } + + public function testNewInstance() + { + $manager = $this->partStreamFilterManagerFactory->newInstance(); + $this->assertInstanceOf( + '\ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager', + $manager + ); + } +} diff --git a/tests/MailMimeParser/Message/Part/Factory/UUEncodedPartFactoryTest.php b/tests/MailMimeParser/Message/Part/Factory/UUEncodedPartFactoryTest.php new file mode 100644 index 00000000..8872347c --- /dev/null +++ b/tests/MailMimeParser/Message/Part/Factory/UUEncodedPartFactoryTest.php @@ -0,0 +1,55 @@ +getMockBuilder('ZBateson\MailMimeParser\Stream\StreamFactory') + ->getMock(); + $mocksdf->expects($this->any()) + ->method('getLimitedPartStream') + ->willReturn(Psr7\stream_for('test')); + $psfmFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\Factory\PartStreamFilterManagerFactory') + ->disableOriginalConstructor() + ->getMock(); + $psfm = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager') + ->disableOriginalConstructor() + ->getMock(); + $psfmFactory + ->method('newInstance') + ->willReturn($psfm); + + $this->uuEncodedPartFactory = new UUEncodedPartFactory($mocksdf, $psfmFactory); + } + + public function testNewInstance() + { + $partBuilder = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $part = $this->uuEncodedPartFactory->newInstance( + $partBuilder, + Psr7\stream_for('test') + ); + $this->assertInstanceOf( + '\ZBateson\MailMimeParser\Message\Part\UUEncodedPart', + $part + ); + } +} diff --git a/tests/MailMimeParser/Message/Part/MessagePartTest.php b/tests/MailMimeParser/Message/Part/MessagePartTest.php new file mode 100644 index 00000000..f9c98e6b --- /dev/null +++ b/tests/MailMimeParser/Message/Part/MessagePartTest.php @@ -0,0 +1,115 @@ +getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager') + ->disableOriginalConstructor() + ->getMock(); + $sf = $this->getMockBuilder('ZBateson\MailMimeParser\Stream\StreamFactory') + ->disableOriginalConstructor() + ->getMock(); + $this->partStreamFilterManager = $psf; + $this->streamFactory = $sf; + } + + private function getMessagePart($handle = 'habibi', $contentHandle = null) + { + if ($contentHandle !== null) { + $contentHandle = Psr7\stream_for($contentHandle); + $this->partStreamFilterManager + ->expects($this->once()) + ->method('setStream'); + $this->partStreamFilterManager + ->expects($this->any()) + ->method('getContentStream') + ->willReturn($contentHandle); + } + return $this->getMockForAbstractClass( + 'ZBateson\MailMimeParser\Message\Part\MessagePart', + [ $this->partStreamFilterManager, $this->streamFactory, Psr7\stream_for($handle), $contentHandle ] + ); + } + + public function testNewInstance() + { + $messagePart = $this->getMessagePart(); + $this->assertNotNull($messagePart); + $this->assertFalse($messagePart->hasContent()); + $this->assertNull($messagePart->getContentResourceHandle()); + $this->assertNull($messagePart->getContent()); + $this->assertNull($messagePart->getParent()); + $this->assertEquals('habibi', stream_get_contents($messagePart->getHandle())); + } + + public function testPartStreamHandle() + { + $messagePart = $this->getMessagePart('mucha agua'); + $this->assertFalse($messagePart->hasContent()); + $this->assertNull($messagePart->getContentResourceHandle()); + $this->assertNotNull($messagePart->getHandle()); + $handle = $messagePart->getHandle(); + $this->assertEquals('mucha agua', stream_get_contents($handle)); + } + + public function testContentStreamHandle() + { + $messagePart = $this->getMessagePart('Que tonta', 'Que tonto'); + $messagePart->method('getContentTransferEncoding') + ->willReturn('wubalubadub-duuuuub'); + $messagePart->method('getCharset') + ->willReturn('wigidiwamwamwazzle'); + + $this->assertTrue($messagePart->hasContent()); + $this->assertSame('Que tonto', stream_get_contents($messagePart->getContentResourceHandle())); + } + + public function testContentStreamHandleWithCustomCharset() + { + $messagePart = $this->getMessagePart('Que tonta', 'Que tonto'); + $messagePart->method('getContentTransferEncoding') + ->willReturn('quoted-printable'); + $messagePart->method('getCharset') + ->willReturn('utf-64'); + + $handle = StreamWrapper::getResource(Psr7\stream_for('Que tonto')); + $this->partStreamFilterManager + ->expects($this->exactly(2)) + ->method('getContentStream') + ->withConsecutive( + ['quoted-printable', 'utf-64', 'a-charset'], + ['quoted-printable', 'utf-64', 'a-charset'] + ) + ->willReturn($handle); + + $this->assertTrue($messagePart->hasContent()); + $this->assertSame('Que tonto', stream_get_contents($messagePart->getContentResourceHandle('a-charset'))); + + fseek($handle, 0); + $messagePart->setCharsetOverride('someCharset', true); + $messagePart->getContentResourceHandle('a-charset'); + } + + public function testGetContent() + { + $messagePart = $this->getMessagePart('habibi', 'sopa di agua con rocas'); + $this->assertEquals('sopa di agua con rocas', $messagePart->getContent()); + } +} diff --git a/tests/MailMimeParser/Message/Part/MimePartTest.php b/tests/MailMimeParser/Message/Part/MimePartTest.php new file mode 100644 index 00000000..dfc2383e --- /dev/null +++ b/tests/MailMimeParser/Message/Part/MimePartTest.php @@ -0,0 +1,572 @@ +mockPartStreamFilterManager = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager') + ->disableOriginalConstructor() + ->getMock(); + $this->mockHeaderFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Header\HeaderFactory') + ->disableOriginalConstructor() + ->getMock(); + $this->mockPartFilterFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilterFactory') + ->disableOriginalConstructor() + ->getMock(); + $this->mockStreamFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Stream\StreamFactory') + ->disableOriginalConstructor() + ->getMock(); + } + + protected function getMockedParameterHeader($name, $value, $parameterValue = null) + { + $header = $this->getMockBuilder('ZBateson\MailMimeParser\Header\ParameterHeader') + ->disableOriginalConstructor() + ->setMethods(['getValue', 'getName', 'getValueFor', 'hasParameter']) + ->getMock(); + $header->method('getName')->willReturn($name); + $header->method('getValue')->willReturn($value); + $header->method('getValueFor')->willReturn($parameterValue); + $header->method('hasParameter')->willReturn(true); + return $header; + } + + protected function getMockedPartBuilder() + { + return $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartBuilder') + ->disableOriginalConstructor() + ->getMock(); + } + + protected function getMockedPartBuilderWithChildren() + { + $pb = $this->getMockedPartBuilder(); + $children = [ + $this->getMockedPartBuilder(), + $this->getMockedPartBuilder(), + $this->getMockedPartBuilder() + ]; + + $nested = $this->getMockedPartBuilder(); + $nested->method('createMessagePart') + ->willReturn($this->newMimePart( + $nested, + Psr7\stream_for('nested') + )); + $children[0]->method('getChildren') + ->willReturn([$nested]); + + foreach ($children as $key => $child) { + $child->method('createMessagePart') + ->willReturn($this->newMimePart( + $child, + Psr7\stream_for('child' . $key) + )); + } + $pb->method('getChildren') + ->willReturn($children); + return $pb; + } + + private function newMimePart($partBuilder, $stream = null, $contentStream = null) + { + return new MimePart( + $this->mockPartStreamFilterManager, + $this->mockStreamFactory, + $this->mockPartFilterFactory, + $this->mockHeaderFactory, + $partBuilder, + $stream, + $contentStream + ); + } + + public function testInstance() + { + $part = $this->newMimePart($this->getMockedPartBuilder()); + $this->assertNotNull($part); + $this->assertInstanceOf('ZBateson\MailMimeParser\Message\Part\MimePart', $part); + $this->assertTrue($part->isMime()); + } + + public function testCreateChildrenAndGetChildren() + { + $part = $this->newMimePart($this->getMockedPartBuilderWithChildren()); + $this->assertEquals(3, $part->getChildCount()); + $this->assertEquals('child0', stream_get_contents($part->getChild(0)->getHandle())); + $this->assertEquals('child1', stream_get_contents($part->getChild(1)->getHandle())); + $this->assertEquals('child2', stream_get_contents($part->getChild(2)->getHandle())); + $children = [ + $part->getChild(0), + $part->getChild(1), + $part->getChild(2) + ]; + $this->assertEquals($children, $part->getChildParts()); + } + + public function testCreateChildrenAndGetParts() + { + $part = $this->newMimePart($this->getMockedPartBuilderWithChildren(), Psr7\stream_for('habibi')); + $this->assertEquals(5, $part->getPartCount()); + + $children = $part->getChildParts(); + $this->assertCount(3, $children); + $nested = $children[0]->getChild(0); + + $this->assertSame($part, $part->getPart(0)); + $this->assertSame($children[0], $part->getPart(1)); + $this->assertSame($nested, $part->getPart(2)); + $this->assertSame($children[1], $part->getPart(3)); + $this->assertSame($children[2], $part->getPart(4)); + + $this->assertEquals('habibi', stream_get_contents($part->getPart(0)->getHandle())); + $this->assertEquals('child0', stream_get_contents($part->getPart(1)->getHandle())); + $this->assertEquals('nested', stream_get_contents($part->getPart(2)->getHandle())); + $this->assertEquals('child1', stream_get_contents($part->getPart(3)->getHandle())); + $this->assertEquals('child2', stream_get_contents($part->getPart(4)->getHandle())); + + $allParts = [ $part, $children[0], $nested, $children[1], $children[2]]; + $this->assertEquals($allParts, $part->getAllParts()); + } + + public function testPartBuilderHeaders() + { + $hf = $this->mockHeaderFactory; + $header = $this->getMockedParameterHeader('Content-Type', 'text/plain', 'utf-8'); + + $pb = $this->getMockedPartBuilder(); + $pb->expects($this->once()) + ->method('getContentType') + ->willReturn($header); + $pb->expects($this->once()) + ->method('getRawHeaders') + ->willReturn(['contenttype' => ['Blah', 'Blah']]); + + $hf->expects($this->never()) + ->method('newInstance'); + + $part = $this->newMimePart($pb); + $this->assertSame($header, $part->getHeader('CONTENT-TYPE')); + $this->assertEquals('text/plain', $part->getHeaderValue('content-type')); + $this->assertEquals('utf-8', $part->getHeaderParameter('CONTent-TyPE', 'charset')); + $this->assertEquals('UTF-8', $part->getCharset()); + $this->assertEquals('text/plain', $part->getContentType()); + } + + public function testGetFilteredParts() + { + $part = $this->newMimePart($this->getMockedPartBuilderWithChildren()); + $parts = $part->getAllParts(); + $filterMock = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilter') + ->disableOriginalConstructor() + ->setMethods(['filter']) + ->getMock(); + $filterMock->expects($this->exactly(5)) + ->method('filter') + ->willReturnOnConsecutiveCalls(false, true, false, true, false); + + $returned = $part->getAllParts($filterMock); + $this->assertCount(2, $returned); + $this->assertEquals([$parts[1], $parts[3]], $returned); + } + + public function testGetFilteredChildParts() + { + $part = $this->newMimePart($this->getMockedPartBuilderWithChildren()); + $parts = $part->getAllParts(); + + $filterMock = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilter') + ->disableOriginalConstructor() + ->setMethods(['filter']) + ->getMock(); + $filterMock->expects($this->exactly(3)) + ->method('filter') + ->willReturnOnConsecutiveCalls(false, true, false); + + $returned = $part->getChildParts($filterMock); + $this->assertCount(1, $returned); + $this->assertEquals([$parts[3]], $returned); + } + + public function testGetUnsetHeader() + { + $part = $this->newMimePart($this->getMockedPartBuilder()); + $this->assertNull($part->getHeader('blah')); + $this->assertEquals('upside-down', $part->getHeaderValue('blah', 'upside-down')); + $this->assertEquals('demigorgon', $part->getHeaderParameter('blah', 'blah', 'demigorgon')); + } + + public function testGetHeaderAndHeaderParameter() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn(['xheader' => ['X-Header', 'Some Value']]); + + $header = $this->getMockedParameterHeader('meen?', 'habibi', 'kochanie'); + $hf = $this->mockHeaderFactory; + $hf->expects($this->once()) + ->method('newInstance') + ->with('X-Header', 'Some Value') + ->willReturn($header); + + $part = $this->newMimePart($pb, Psr7\stream_for('habibi')); + $this->assertEquals($header, $part->getHeader('X-header')); + $this->assertEquals('habibi', $part->getHeaderValue('x-HEADER')); + $this->assertEquals('kochanie', $part->getHeaderParameter('x-header', 'anything')); + } + + public function testGetContentDisposition() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contentdisposition' => ['Content-Disposition', 'attachment; filename=bin-bashy.jpg'] + ]); + + $header = $this->getMockedParameterHeader('meen?', 'habibi'); + $hf = $this->mockHeaderFactory; + $hf->expects($this->once()) + ->method('newInstance') + ->with('Content-Disposition', 'attachment; filename=bin-bashy.jpg') + ->willReturn($header); + + $part = $this->newMimePart($pb, Psr7\stream_for('habibi')); + $this->assertSame($header, $part->getHeader('CONTENT-DISPOSITION')); + $this->assertEquals('habibi', $part->getContentDisposition()); + } + + public function testGetContentTransferEncoding() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contenttransferencoding' => ['Content-Transfer-Encoding', 'base64'] + ]); + + $header = $this->getMockedParameterHeader('meen?', 'HABIBI'); + $hf = $this->mockHeaderFactory; + $hf->expects($this->once()) + ->method('newInstance') + ->with('Content-Transfer-Encoding', 'base64') + ->willReturn($header); + + $part = $this->newMimePart($pb, Psr7\stream_for('habibi')); + $this->assertSame($header, $part->getHeader('CONTENT-TRANSFER_ENCODING')); + $this->assertEquals('habibi', $part->getContentTransferEncoding()); + } + + public function testGetCharset() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Content-Type', 'text/plain; charset=blah'] + ]); + + $header = $this->getMockedParameterHeader('content-type', 'text/plain', 'blah'); + $hf = $this->mockHeaderFactory; + $hf->expects($this->once()) + ->method('newInstance') + ->with('Content-Type', 'text/plain; charset=blah') + ->willReturn($header); + + $part = $this->newMimePart($pb); + $this->assertEquals('BLAH', $part->getCharset()); + } + + public function testGetDefaultCharsetForTextPlainAndTextHtml() + { + $pbText = $this->getMockedPartBuilder(); + $pbText->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Content-Type', 'text/plain'] + ]); + $pbHtml = $this->getMockedPartBuilder(); + $pbHtml->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Content-Type', 'text/html'] + ]); + + $headerText = $this->getMockedParameterHeader('content-type', 'text/plain'); + $headerHtml = $this->getMockedParameterHeader('content-type', 'text/html'); + + $hf = $this->mockHeaderFactory; + $hf->expects($this->exactly(2)) + ->method('newInstance') + ->withConsecutive(['Content-Type', 'text/plain'], ['Content-Type', 'text/html']) + ->willReturnOnConsecutiveCalls($headerText, $headerHtml); + + $partText = $this->newMimePart($pbText); + $partHtml = $this->newMimePart($pbHtml); + + $this->assertEquals('ISO-8859-1', $partText->getCharset()); + $this->assertEquals('ISO-8859-1', $partHtml->getCharset()); + } + + public function testGetNullCharsetForNonTextPlainOrHtmlPart() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Content-Type', 'text/rtf'] + ]); + + $header = $this->getMockedParameterHeader('content-type', 'text/rtf'); + $hf = $this->mockHeaderFactory; + $hf->expects($this->once()) + ->method('newInstance') + ->with('Content-Type', 'text/rtf') + ->willReturn($header); + + $part = $this->newMimePart($pb); + $this->assertNull($part->getCharset()); + } + + public function testUsesTransferEncodingAndCharsetForStreamFilter() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Content-Type', 'text/plain; charset=wingding'], + 'contenttransferencoding' => ['Content-Transfer-Encoding', 'klingon'] + ]); + $headerType = $this->getMockedParameterHeader('Content-Type', 'text/plain', 'wingding'); + $headerEnc = $this->getMockedParameterHeader('Content-Transfer-Encoding', 'klingon'); + + $hf = $this->mockHeaderFactory; + $hf->method('newInstance') + ->willReturnMap([ + ['Content-Type', 'text/plain; charset=wingding', $headerType], + ['Content-Transfer-Encoding', 'klingon', $headerEnc] + ]); + + $manager = $this->mockPartStreamFilterManager; + $manager->expects($this->once()) + ->method('getContentStream') + ->with('klingon', 'WINGDING', 'UTF-8') + ->willReturn(Psr7\stream_for('totally not null')); + + $part = $this->newMimePart($pb, Psr7\stream_for('habibi'), Psr7\stream_for('blah')); + $this->assertEquals('WINGDING', $part->getCharset()); + $this->assertEquals('klingon', $part->getContentTransferEncoding()); + $this->assertNotNull($part->getContentResourceHandle()); + } + + public function testIsTextIsMultiPartForNonTextNonMultipart() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Not', 'Important'] + ]); + + $header = $this->getMockedParameterHeader('Content-Type', 'stuff/blooh'); + $hf = $this->mockHeaderFactory; + $hf->method('newInstance') + ->willReturn($header); + + $part = $this->newMimePart($pb); + $this->assertFalse($part->isMultiPart()); + $this->assertFalse($part->isTextPart()); + } + + public function testIsTextForTextPlain() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Not', 'Important'] + ]); + + $header = $this->getMockedParameterHeader('Content-Type', 'text/plain'); + $hf = $this->mockHeaderFactory; + $hf->method('newInstance') + ->willReturn($header); + + $part = $this->newMimePart($pb); + $this->assertFalse($part->isMultiPart()); + $this->assertTrue($part->isTextPart()); + } + + public function testIsTextForTextHtml() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Not', 'Important'] + ]); + + $header = $this->getMockedParameterHeader('Content-Type', 'text/html'); + $hf = $this->mockHeaderFactory; + $hf->method('newInstance') + ->willReturn($header); + + $part = $this->newMimePart($pb); + $this->assertFalse($part->isMultiPart()); + $this->assertTrue($part->isTextPart()); + } + + public function testIsTextForTextMimeTypeWithCharset() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Not', 'Important'] + ]); + + $header = $this->getMockedParameterHeader('Content-Type', 'text/blah', 'utf-8'); + $header->expects($this->once()) + ->method('getValueFor') + ->with('charset'); + + $hf = $this->mockHeaderFactory; + $hf->method('newInstance') + ->willReturn($header); + + $part = $this->newMimePart($pb); + $this->assertFalse($part->isMultiPart()); + $this->assertTrue($part->isTextPart()); + } + + public function testIsTextForTextMimeTypeWithoutCharset() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Not', 'Important'] + ]); + + $header = $this->getMockedParameterHeader('Content-Type', 'text/blah'); + $header->expects($this->once()) + ->method('getValueFor') + ->with('charset'); + + $hf = $this->mockHeaderFactory; + $hf->method('newInstance') + ->willReturn($header); + + $part = $this->newMimePart($pb); + $this->assertFalse($part->isMultiPart()); + $this->assertFalse($part->isTextPart()); + } + + public function testIsMultipartForMultipartRelated() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Not', 'Important'] + ]); + + $header = $this->getMockedParameterHeader('Content-Type', 'multipart/related'); + $hf = $this->mockHeaderFactory; + $hf->method('newInstance') + ->willReturn($header); + + $part = $this->newMimePart($pb); + $this->assertTrue($part->isMultiPart()); + $this->assertFalse($part->isTextPart()); + } + + public function testIsMultipartForMultipartAnything() + { + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn([ + 'contenttype' => ['Not', 'Important'] + ]); + + $header = $this->getMockedParameterHeader('Content-Type', 'multipart/anything'); + $hf = $this->mockHeaderFactory; + $hf->method('newInstance') + ->willReturn($header); + + $part = $this->newMimePart($pb); + $this->assertTrue($part->isMultiPart()); + $this->assertFalse($part->isTextPart()); + } + + public function testGetAllPartsByMimeType() + { + $hf = $this->mockHeaderFactory; + $pf = $this->mockPartFilterFactory; + $filter = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilter') + ->disableOriginalConstructor() + ->getMock(); + $filter->expects($this->exactly(5)) + ->method('filter') + ->willReturnOnConsecutiveCalls(true, true, false, false, false); + + $pf->expects($this->once()) + ->method('newFilterFromContentType') + ->with('awww geez') + ->willReturn($filter); + + $part = $this->newMimePart($this->getMockedPartBuilderWithChildren()); + $parts = $part->getAllPartsByMimeType('awww geez'); + $this->assertCount(2, $parts); + } + + public function testGetPartByMimeType() + { + $hf = $this->mockHeaderFactory; + $pf = $this->mockPartFilterFactory; + $filter = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilter') + ->disableOriginalConstructor() + ->getMock(); + $filter->expects($this->exactly(10)) + ->method('filter') + ->willReturnOnConsecutiveCalls( + true, false, false, true, false, + true, false, false, true, false + ); + + $pf->expects($this->exactly(2)) + ->method('newFilterFromContentType') + ->with('awww geez') + ->willReturn($filter); + + $part = $this->newMimePart($this->getMockedPartBuilderWithChildren()); + $this->assertSame($part, $part->getPartByMimeType('awww geez')); + $this->assertSame($part->getPart(3), $part->getPartByMimeType('awww geez', 1)); + } + + public function testGetCountOfPartsByMimeType() + { + $hf = $this->mockHeaderFactory; + $pf = $this->mockPartFilterFactory; + $filter = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilter') + ->disableOriginalConstructor() + ->getMock(); + $filter->expects($this->exactly(5)) + ->method('filter') + ->willReturnOnConsecutiveCalls(true, true, false, false, true); + + $pf->expects($this->once()) + ->method('newFilterFromContentType') + ->with('awww geez, Rick') + ->willReturn($filter); + + $part = $this->newMimePart($this->getMockedPartBuilderWithChildren()); + $this->assertEquals(3, $part->getCountOfPartsByMimeType('awww geez, Rick')); + } +} diff --git a/tests/MailMimeParser/Message/Part/NonMimePartTest.php b/tests/MailMimeParser/Message/Part/NonMimePartTest.php new file mode 100644 index 00000000..3a5131c4 --- /dev/null +++ b/tests/MailMimeParser/Message/Part/NonMimePartTest.php @@ -0,0 +1,32 @@ +getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager') + ->disableOriginalConstructor() + ->getMock(); + $sf = $this->getMockBuilder('ZBateson\MailMimeParser\Stream\StreamFactory') + ->disableOriginalConstructor() + ->getMock(); + $part = new NonMimePart($mgr, $sf); + $this->assertTrue($part->isTextPart()); + $this->assertFalse($part->isMime()); + $this->assertEquals('text/plain', $part->getContentType()); + $this->assertEquals('inline', $part->getContentDisposition()); + $this->assertEquals('7bit', $part->getContentTransferEncoding()); + } +} diff --git a/tests/MailMimeParser/Message/Part/PartBuilderTest.php b/tests/MailMimeParser/Message/Part/PartBuilderTest.php new file mode 100644 index 00000000..4e121ca3 --- /dev/null +++ b/tests/MailMimeParser/Message/Part/PartBuilderTest.php @@ -0,0 +1,449 @@ +mockHeaderFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Header\HeaderFactory') + ->disableOriginalConstructor() + ->setMethods(['newInstance']) + ->getMock(); + $this->mockMessagePartFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\Factory\MessagePartFactory') + ->disableOriginalConstructor() + ->setMethods(['newInstance']) + ->getMock(); + } + + public function testCanHaveHeaders() + { + $mockHeader = $this->getMockBuilder('ZBateson\MailMimeParser\Header\ParameterHeader') + ->disableOriginalConstructor() + ->setMethods(['getValueFor']) + ->getMock(); + $mockHeader->expects($this->any()) + ->method('getValueFor') + ->with('boundary') + ->willReturn('Castle Black'); + + $this->mockHeaderFactory + ->expects($this->any()) + ->method('newInstance') + ->willReturn($mockHeader); + + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + + $this->assertTrue($instance->canHaveHeaders()); + + $parent = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $parent->addHeader('CONTENT-TYPE', 'kookoo-keekee'); + $parent->addChild($instance); + + $parent->setEndBoundaryFound('--Castle Black--'); + $this->assertFalse($instance->canHaveHeaders()); + } + + public function testAddChildren() + { + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $children = [ + new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ), + new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ) + ]; + foreach ($children as $child) { + $instance->addChild($child); + } + $this->assertEquals($children, $instance->getChildren()); + $this->assertSame($instance, $children[0]->getParent()); + $this->assertSame($instance, $children[1]->getParent()); + } + + public function testAddAndGetRawHeaders() + { + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $instance->addHeader('Mime-VERSION', '42'); + $instance->addHeader('Content-TYPE', 'text/blah; blooh'); + $instance->addHeader('X-Northernmost-Castle', 'Castle black'); + + $expectedHeaders = [ + 'mimeversion' => ['Mime-VERSION', '42'], + 'contenttype' => ['Content-TYPE', 'text/blah; blooh'], + 'xnorthernmostcastle' => ['X-Northernmost-Castle', 'Castle black'] + ]; + $this->assertEquals($expectedHeaders, $instance->getRawHeaders()); + } + + public function testAddMimeVersionHeader() + { + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $instance->addHeader('Mime-VERSION', '42'); + $this->assertTrue($instance->isMime()); + } + + public function testAddContentTypeHeaderIsMime() + { + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $instance->addHeader('CONTENT-TYPE', '42'); + $this->assertTrue($instance->isMime()); + } + + public function testGetContentType() + { + $this->mockHeaderFactory + ->expects($this->atLeastOnce()) + ->method('newInstance') + ->willReturn(true); + + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $instance->addHeader('CONTENT-TYPE', '42'); + $this->assertTrue($instance->getContentType()); + } + + public function testGetMimeBoundary() + { + $mockHeader = $this->getMockBuilder('ZBateson\MailMimeParser\Header\ParameterHeader') + ->disableOriginalConstructor() + ->setMethods(['getValueFor']) + ->getMock(); + $mockHeader->expects($this->any()) + ->method('getValueFor') + ->with('boundary') + ->willReturn('Castle Black'); + + $this->mockHeaderFactory + ->expects($this->atLeastOnce()) + ->method('newInstance') + ->willReturn($mockHeader); + + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $instance->addHeader('CONTENT-TYPE', 'Snow and Ice'); + $this->assertEquals('Castle Black', $instance->getMimeBoundary()); + } + + public function testIsMultiPart() + { + $mockHeader = $this->getMockBuilder('ZBateson\MailMimeParser\Header\ParameterHeader') + ->disableOriginalConstructor() + ->setMethods(['getValue']) + ->getMock(); + $mockHeader->expects($this->any()) + ->method('getValue') + ->willReturnOnConsecutiveCalls('multipart/kookoo', 'text/plain'); + + $this->mockHeaderFactory + ->expects($this->atLeastOnce()) + ->method('newInstance') + ->willReturn($mockHeader); + + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $instance->addHeader('CONTENT-TYPE', 'kookoo-keekee'); + $this->assertTrue($instance->isMultiPart()); + $this->assertFalse($instance->isMultiPart()); + } + + public function testSetEndBoundaryFound() + { + $mockHeader = $this->getMockBuilder('ZBateson\MailMimeParser\Header\ParameterHeader') + ->disableOriginalConstructor() + ->setMethods(['getValueFor']) + ->getMock(); + $mockHeader->expects($this->any()) + ->method('getValueFor') + ->with('boundary') + ->willReturn('Castle Black'); + + $this->mockHeaderFactory + ->expects($this->any()) + ->method('newInstance') + ->willReturnOnConsecutiveCalls($mockHeader); + + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + + $instance->addHeader('CONTENT-TYPE', 'kookoo-keekee'); + + $this->assertFalse($instance->isParentBoundaryFound()); + $this->assertFalse($instance->setEndBoundaryFound('Somewhere... obvs not Castle Black')); + $this->assertFalse($instance->setEndBoundaryFound('Castle Black')); + $this->assertTrue($instance->setEndBoundaryFound('--Castle Black')); + $this->assertFalse($instance->isParentBoundaryFound()); + $this->assertTrue($instance->setEndBoundaryFound('--Castle Black--')); + + $child = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'tigris' + ); + $instance->addChild($child); + $this->assertEquals($instance, $child->getParent()); + $this->assertCount(0, $instance->getChildren()); + $this->assertFalse($child->canHaveHeaders()); + } + + public function testSetEndBoundaryFoundWithParent() + { + $mockParentHeader = $this->getMockBuilder('ZBateson\MailMimeParser\Header\ParameterHeader') + ->disableOriginalConstructor() + ->setMethods(['getValueFor']) + ->getMock(); + $mockParentHeader->expects($this->any()) + ->method('getValueFor') + ->with('boundary') + ->willReturn('King\'s Landing'); + + $mockHeader = $this->getMockBuilder('ZBateson\MailMimeParser\Header\ParameterHeader') + ->disableOriginalConstructor() + ->setMethods(['getValueFor']) + ->getMock(); + $mockHeader->expects($this->any()) + ->method('getValueFor') + ->with('boundary') + ->willReturn(null); + + $this->mockHeaderFactory + ->expects($this->atLeastOnce()) + ->method('newInstance') + ->willReturnOnConsecutiveCalls($mockHeader, $mockParentHeader); + + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $parent = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $parent->addChild($instance); + + $instance->addHeader('CONTENT-TYPE', 'kookoo-keekee'); + $parent->addHeader('CONTENT-TYPE', 'keekee-kookoo'); + + $this->assertSame($parent, $instance->getParent()); + $this->assertFalse($instance->isParentBoundaryFound()); + $this->assertFalse($instance->setEndBoundaryFound('Somewhere... obvs not Castle Black')); + $this->assertFalse($instance->setEndBoundaryFound('King\'s Landing')); + $this->assertTrue($instance->setEndBoundaryFound('--King\'s Landing')); + $this->assertTrue($instance->isParentBoundaryFound()); + } + + public function testSetEof() + { + $mockParentHeader = $this->getMockBuilder('ZBateson\MailMimeParser\Header\ParameterHeader') + ->disableOriginalConstructor() + ->setMethods(['getValueFor']) + ->getMock(); + $mockParentHeader->expects($this->any()) + ->method('getValueFor') + ->with('boundary') + ->willReturn('King\'s Landing'); + + $mockHeader = $this->getMockBuilder('ZBateson\MailMimeParser\Header\ParameterHeader') + ->disableOriginalConstructor() + ->setMethods(['getValueFor']) + ->getMock(); + $mockHeader->expects($this->any()) + ->method('getValueFor') + ->with('boundary') + ->willReturn(null); + + $this->mockHeaderFactory + ->expects($this->atLeastOnce()) + ->method('newInstance') + ->willReturnOnConsecutiveCalls($mockHeader, $mockParentHeader); + + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $parent = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $parent->addChild($instance); + + $instance->addHeader('CONTENT-TYPE', 'kookoo-keekee'); + $parent->addHeader('CONTENT-TYPE', 'keekee-kookoo'); + + $this->assertSame($parent, $instance->getParent()); + $this->assertFalse($instance->isParentBoundaryFound()); + $this->assertFalse($instance->setEndBoundaryFound('Somewhere... obvs not Castle Black')); + $this->assertFalse($instance->setEndBoundaryFound('Szprotka')); + $this->assertFalse($instance->setEndBoundaryFound('--szprotka')); + $this->assertFalse($instance->isParentBoundaryFound()); + $instance->setEof(); + $this->assertTrue($instance->isParentBoundaryFound()); + $this->assertTrue($parent->isParentBoundaryFound()); + } + + public function testSetStreamPartPosAndGetFilename() + { + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $instance->setStreamPartStartPos(42); + $instance->setStreamPartEndPos(84); + $this->assertEquals(42, $instance->getStreamPartStartOffset()); + $this->assertEquals(42, $instance->getStreamPartLength()); + } + + public function testSetStreamContentPosAndGetFilename() + { + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'tigris' + ); + $instance->setStreamPartStartPos(11); + $instance->setStreamContentStartPos(42); + $instance->setStreamPartAndContentEndPos(84); + $this->assertEquals(11, $instance->getStreamPartStartOffset()); + $this->assertEquals(84 - 11, $instance->getStreamPartLength()); + $this->assertEquals(42, $instance->getStreamContentStartOffset()); + $this->assertEquals(84 - 42, $instance->getStreamContentLength()); + } + + public function testSetStreamContentPosAndGetFilenameWithParent() + { + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'tigris' + ); + $parent = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $super = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'vistula' + ); + $parent->addChild($instance); + $super->addChild($parent); + + $super->setStreamPartStartPos(0); + $super->setStreamContentStartPos(3); + $super->setStreamPartAndContentEndPos(3); + + $parent->setStreamPartStartPos(11); + $parent->setStreamContentStartPos(13); + $parent->setStreamPartAndContentEndPos(20); + + $instance->setStreamPartStartPos(22); + $instance->setStreamContentStartPos(42); + $instance->setStreamPartAndContentEndPos(84); + + $this->assertEquals(42 - $parent->getStreamPartStartOffset(), $instance->getStreamContentStartOffset()); + $this->assertEquals(84 - 42, $instance->getStreamContentLength()); + $this->assertEquals(22 - $parent->getStreamPartStartOffset(), $instance->getStreamPartStartOffset()); + $this->assertEquals(84 - 22, $instance->getStreamPartLength()); + + $this->assertEquals(13, $parent->getStreamContentStartOffset()); + $this->assertEquals(20 - 13, $parent->getStreamContentLength()); + $this->assertEquals(11, $parent->getStreamPartStartOffset()); + $this->assertEquals(84 - 11, $parent->getStreamPartLength()); + + $this->assertEquals(3, $super->getStreamContentStartOffset()); + $this->assertEquals(0, $super->getStreamContentLength()); + $this->assertEquals(0, $super->getStreamPartStartOffset()); + $this->assertEquals(84, $super->getStreamPartLength()); + } + + public function testSetAndGetProperties() + { + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + $instance->setProperty('island', 'Westeros'); + $instance->setProperty('capital', 'King\'s Landing'); + $this->assertSame('Westeros', $instance->getProperty('island')); + $this->assertSame('King\'s Landing', $instance->getProperty('capital')); + $this->assertNull($instance->getProperty('Joffrey\'s kindness')); + } + + public function testCreateMessagePart() + { + $stream = Psr7\stream_for('thingsnstuff'); + $instance = new PartBuilder( + $this->mockHeaderFactory, + $this->mockMessagePartFactory, + 'euphrates' + ); + + $this->mockMessagePartFactory->expects($this->once()) + ->method('newInstance') + ->with($instance, $stream) + ->willReturn(true); + $this->assertTrue($instance->createMessagePart($stream)); + } +} diff --git a/tests/MailMimeParser/Message/Part/PartStreamFilterManagerTest.php b/tests/MailMimeParser/Message/Part/PartStreamFilterManagerTest.php new file mode 100644 index 00000000..393350a5 --- /dev/null +++ b/tests/MailMimeParser/Message/Part/PartStreamFilterManagerTest.php @@ -0,0 +1,204 @@ +getMockBuilder('ZBateson\MailMimeParser\Stream\StreamFactory') + ->getMock(); + $this->partStreamFilterManager = new PartStreamFilterManager($mocksdf); + $this->mockStreamFactory = $mocksdf; + } + + public function testAttachQuotedPrintableDecoder() + { + $stream = Psr7\stream_for('test'); + $this->mockStreamFactory->expects($this->exactly(1)) + ->method('newQuotedPrintableStream') + ->with($stream) + ->willReturn($stream); + $this->partStreamFilterManager->setStream($stream); + $managerStream = $this->partStreamFilterManager->getContentStream('quoted-printable', null, null); + $this->assertInstanceOf('\GuzzleHttp\Psr7\CachingStream', $managerStream); + $this->assertEquals('test', $managerStream->getContents()); + } + + public function testAttachBase64Decoder() + { + $stream = Psr7\stream_for('test'); + $this->mockStreamFactory->expects($this->exactly(1)) + ->method('newBase64Stream') + ->with($stream) + ->willReturn($stream); + $this->partStreamFilterManager->setStream($stream); + $managerStream = $this->partStreamFilterManager->getContentStream('base64', null, null); + $this->assertInstanceOf('\GuzzleHttp\Psr7\CachingStream', $managerStream); + $this->assertEquals('test', $managerStream->getContents()); + } + + public function testAttachUUEncodeDecoder() + { + $stream = Psr7\stream_for('test'); + $this->mockStreamFactory->expects($this->exactly(1)) + ->method('newUUStream') + ->with($stream) + ->willReturn($stream); + $this->partStreamFilterManager->setStream($stream); + $managerStream = $this->partStreamFilterManager->getContentStream('x-uuencode', null, null); + $this->assertInstanceOf('\GuzzleHttp\Psr7\CachingStream', $managerStream); + $this->assertEquals('test', $managerStream->getContents()); + } + + public function testAttachCharsetConversionDecoder() + { + $stream = Psr7\stream_for('test'); + $this->mockStreamFactory->expects($this->exactly(1)) + ->method('newCharsetStream') + ->with($stream, 'US-ASCII', 'UTF-8') + ->willReturn($stream); + $this->partStreamFilterManager->setStream($stream); + $managerStream = $this->partStreamFilterManager->getContentStream(null, 'US-ASCII', 'UTF-8'); + $this->assertInstanceOf('\GuzzleHttp\Psr7\CachingStream', $managerStream); + $this->assertEquals('test', $managerStream->getContents()); + } + + public function testReAttachTransferEncodingDecoder() + { + $stream = Psr7\stream_for('test'); + $this->mockStreamFactory->expects($this->exactly(1)) + ->method('newQuotedPrintableStream') + ->with($stream) + ->willReturn($stream); + $stream->rewind(); + + $stream2 = Psr7\stream_for('test2'); + $stream3 = Psr7\stream_for('test3'); + $this->mockStreamFactory->expects($this->exactly(2)) + ->method('newUUStream') + ->with($stream) + ->willReturnOnConsecutiveCalls($stream2, $stream3); + $this->partStreamFilterManager->setStream($stream); + + $manager = $this->partStreamFilterManager; + $this->assertEquals('test2', $manager->getContentStream('x-uuencode', null, null)->getContents()); + $this->assertEquals('test2', $manager->getContentStream('x-uuencode', null, null)->getContents()); + $this->assertEquals('test2', $manager->getContentStream('x-uuencode', null, null)->getContents()); + + $this->assertEquals('test', $manager->getContentStream('quoted-printable', null, null)->getContents()); + $this->assertEquals('test', $manager->getContentStream('quoted-printable', null, null)->getContents()); + + $this->assertEquals('test3', $manager->getContentStream('x-uuencode', null, null)->getContents()); + } + + public function testReAttachCharsetConversionDecoder() + { + $stream = Psr7\stream_for('test'); + $this->mockStreamFactory->expects($this->exactly(4)) + ->method('newCharsetStream') + ->withConsecutive( + [$stream, 'US-ASCII', 'UTF-8'], + [$stream, 'US-ASCII', 'WINDOWS-1252'], + [$stream, 'ISO-8859-1', 'WINDOWS-1252'], + [$stream, 'WINDOWS-1252', 'UTF-8'] + ) + ->willReturn($stream); + $this->partStreamFilterManager->setStream($stream); + + $manager = $this->partStreamFilterManager; + $this->assertEquals('test', $manager->getContentStream(null, 'US-ASCII', 'UTF-8')->getContents()); + $this->assertEquals('test', $manager->getContentStream(null, 'US-ASCII', 'UTF-8')->getContents()); + $this->assertEquals('test', $manager->getContentStream(null, 'US-ASCII', 'WINDOWS-1252')->getContents()); + $this->assertEquals('test', $manager->getContentStream(null, 'ISO-8859-1', 'WINDOWS-1252')->getContents()); + $this->assertEquals('test', $manager->getContentStream(null, 'ISO-8859-1', 'WINDOWS-1252')->getContents()); + $this->assertEquals('test', $manager->getContentStream(null, 'WINDOWS-1252', 'UTF-8')->getContents()); + } + + public function testAttachCharsetConversionAndTransferEncodingDecoder() + { + $stream = Psr7\stream_for('test'); + $this->mockStreamFactory->expects($this->exactly(1)) + ->method('newCharsetStream') + ->with($this->anything(), 'US-ASCII', 'UTF-8') + ->willReturn($stream); + $this->mockStreamFactory->expects($this->exactly(1)) + ->method('newQuotedPrintableStream') + ->with($stream) + ->willReturn($stream); + $this->partStreamFilterManager->setStream($stream); + + $manager = $this->partStreamFilterManager; + $this->assertEquals('test', $manager->getContentStream('quoted-printable', 'US-ASCII', 'UTF-8')->getContents()); + $this->assertEquals('test', $manager->getContentStream('quoted-printable', 'US-ASCII', 'UTF-8')->getContents()); + $this->assertEquals('test', $manager->getContentStream('quoted-printable', 'US-ASCII', 'UTF-8')->getContents()); + } + + /*public function testReset() + { + $callCount = 0; + PartStreamFilterManagerTestStreamFilter::setOnCreateCallback( + function ($filtername, $params) use (&$callCount) { + ++$callCount; + } + ); + + $closeCount = 0; + PartStreamFilterManagerTestStreamFilter::setOnCloseCallback( + function ($filtername, $params) use (&$closeCount) { + ++$closeCount; + } + ); + + $manager = $this->partStreamFilterManager; + $manager->getContentStream('quoted-printable', 'US-ASCII', 'UTF-8'); + $manager->reset(); + + $this->assertEquals(2, $callCount); + $this->assertEquals(2, $closeCount); + + $manager->getContentStream('quoted-printable', 'US-ASCII', 'UTF-8'); + + $this->assertEquals(4, $callCount); + $this->assertEquals(2, $closeCount); + } + + public function testResetByAttachingDifferentHandle() + { + $callCount = 0; + PartStreamFilterManagerTestStreamFilter::setOnCreateCallback( + function ($filtername, $params) use (&$callCount) { + ++$callCount; + } + ); + + $closeCount = 0; + PartStreamFilterManagerTestStreamFilter::setOnCloseCallback( + function ($filtername, $params) use (&$closeCount) { + ++$closeCount; + } + ); + + $manager = $this->partStreamFilterManager; + $manager->getContentStream('quoted-printable', 'US-ASCII', 'UTF-16'); + $manager->setContentUrl('php://temp'); + $manager->getContentStream('quoted-printable', 'US-ASCII', 'UTF-16'); + + $this->assertEquals(4, $callCount); + $this->assertEquals(2, $closeCount); + }*/ +} diff --git a/tests/MailMimeParser/Message/Part/UUEncodedPartTest.php b/tests/MailMimeParser/Message/Part/UUEncodedPartTest.php new file mode 100644 index 00000000..a2037670 --- /dev/null +++ b/tests/MailMimeParser/Message/Part/UUEncodedPartTest.php @@ -0,0 +1,51 @@ +getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager') + ->disableOriginalConstructor() + ->getMock(); + $sf = $this->getMockBuilder('ZBateson\MailMimeParser\Stream\StreamFactory') + ->disableOriginalConstructor() + ->getMock(); + + $pb = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartBuilder') + ->disableOriginalConstructor() + ->getMock(); + $pb->expects($this->exactly(2)) + ->method('getProperty') + ->willReturnCallback(function ($param) { + $return = ['filename' => 'wubalubadubduuuuuub!', 'mode' => 0666]; + $this->assertArrayHasKey($param, $return); + return $return[$param]; + }); + + $part = new UUEncodedPart( + $mgr, + $sf, + $pb, + Psr7\stream_for('Stuff') + ); + $this->assertFalse($part->isTextPart()); + $this->assertFalse($part->isMime()); + $this->assertEquals('application/octet-stream', $part->getContentType()); + $this->assertEquals('attachment', $part->getContentDisposition()); + $this->assertEquals('x-uuencode', $part->getContentTransferEncoding()); + $this->assertEquals(0666, $part->getUnixFileMode()); + $this->assertEquals('wubalubadubduuuuuub!', $part->getFilename()); + } +} diff --git a/tests/MailMimeParser/Message/PartFilterFactoryTest.php b/tests/MailMimeParser/Message/PartFilterFactoryTest.php new file mode 100644 index 00000000..1044fd1d --- /dev/null +++ b/tests/MailMimeParser/Message/PartFilterFactoryTest.php @@ -0,0 +1,89 @@ +partFilterFactory = new PartFilterFactory(); + } + + public function testNewFilterFromContentType() + { + $pf = $this->partFilterFactory->newFilterFromContentType('text/html'); + $this->assertNotNull($pf); + $this->assertInstanceOf('ZBateson\MailMimeParser\Message\PartFilter', $pf); + $this->assertEquals(PartFilter::FILTER_OFF, $pf->multipart); + $this->assertEquals(PartFilter::FILTER_OFF, $pf->textpart); + $this->assertEquals(PartFilter::FILTER_EXCLUDE, $pf->signedpart); + $this->assertEquals( + [ PartFilter::FILTER_INCLUDE => [ 'Content-Type' => 'text/html' ] ], + $pf->headers + ); + } + + public function testNewFilterFromInlineContentType() + { + $pf = $this->partFilterFactory->newFilterFromInlineContentType('text/html'); + $this->assertNotNull($pf); + $this->assertInstanceOf('ZBateson\MailMimeParser\Message\PartFilter', $pf); + $this->assertEquals(PartFilter::FILTER_OFF, $pf->multipart); + $this->assertEquals(PartFilter::FILTER_OFF, $pf->textpart); + $this->assertEquals(PartFilter::FILTER_EXCLUDE, $pf->signedpart); + $this->assertEquals( + [ + PartFilter::FILTER_INCLUDE => [ 'Content-Type' => 'text/html' ], + PartFilter::FILTER_EXCLUDE => [ 'Content-Disposition' => 'attachment' ] + ], + $pf->headers + ); + } + + public function testNewFilterFromDisposition() + { + $pf = $this->partFilterFactory->newFilterFromDisposition('inline', PartFilter::FILTER_EXCLUDE); + $this->assertNotNull($pf); + $this->assertInstanceOf('ZBateson\MailMimeParser\Message\PartFilter', $pf); + $this->assertEquals(PartFilter::FILTER_EXCLUDE, $pf->multipart); + $this->assertEquals(PartFilter::FILTER_OFF, $pf->textpart); + $this->assertEquals(PartFilter::FILTER_EXCLUDE, $pf->signedpart); + $this->assertEquals( + [ + PartFilter::FILTER_INCLUDE => [ 'Content-Disposition' => 'inline' ] + ], + $pf->headers + ); + } + + public function testNewFilterFromArray() + { + $headers = [ + PartFilter::FILTER_INCLUDE => [ 'test' => 'blah' ] + ]; + $pf = $this->partFilterFactory->newFilterFromArray([ + 'headers' => $headers, + 'multipart' => PartFilter::FILTER_EXCLUDE, + 'textpart' => PartFilter::FILTER_EXCLUDE, + 'signedpart' => PartFilter::FILTER_INCLUDE + ]); + $this->assertNotNull($pf); + $this->assertInstanceOf('ZBateson\MailMimeParser\Message\PartFilter', $pf); + $this->assertEquals(PartFilter::FILTER_EXCLUDE, $pf->multipart); + $this->assertEquals(PartFilter::FILTER_EXCLUDE, $pf->textpart); + $this->assertEquals(PartFilter::FILTER_INCLUDE, $pf->signedpart); + $this->assertEquals($headers, $pf->headers); + } +} diff --git a/tests/MailMimeParser/Message/PartFilterTest.php b/tests/MailMimeParser/Message/PartFilterTest.php index 0cc117c2..5add5118 100644 --- a/tests/MailMimeParser/Message/PartFilterTest.php +++ b/tests/MailMimeParser/Message/PartFilterTest.php @@ -4,7 +4,7 @@ use PHPUnit_Framework_TestCase; /** - * Description of NonMimePartTest + * PartFilterTest * * @group PartFilter * @group Message @@ -15,20 +15,26 @@ class PartFilterTest extends PHPUnit_Framework_TestCase { private $parts = []; - protected function getMockedPartWithContentType($mimeType, $disposition = null) + protected function getMockedPartWithContentType($mimeType, $disposition = null, $isText = false) { - $part = $this->getMockBuilder('ZBateson\MailMimeParser\Message\MimePart') + $part = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\MimePart') ->disableOriginalConstructor() - ->setMethods(['setRawHeader', 'getHeader', 'getHeaderValue', 'getHeaderParameter', 'getContentResourceHandle', 'getParent']) + ->setMethods([ + '__destruct', + 'setRawHeader', + 'getHeader', + 'getHeaderValue', + 'getHeaderParameter', + 'getContentResourceHandle', + 'getParent', + 'getContentType', + 'getContentDisposition', + 'isTextPart', + ]) ->getMock(); - $part->method('getHeaderValue')->will($this->returnCallback(function($param, $defaultValue = null) use ($mimeType, $disposition) { - if (strcasecmp($param, 'Content-Type') === 0) { - return $mimeType; - } elseif (strcasecmp($param, 'Content-Disposition') === 0) { - return $disposition; - } - return $defaultValue; - })); + $part->method('getContentType')->willReturn($mimeType); + $part->method('getContentDisposition')->willReturn($disposition); + $part->method('isTextPart')->willReturn($isText); return $part; } @@ -47,11 +53,11 @@ public function setUp() $signedPart = $this->getMockedSignedPart(); $signedPartParent = $signedPart->getParent(); $this->parts = [ - $this->getMockedPartWithContentType('text/html'), + $this->getMockedPartWithContentType('text/html', null, true), $this->getMockedPartWithContentType('multipart/alternative', 'inline'), - $this->getMockedPartWithContentType('text/html', 'inline'), - $this->getMockedPartWithContentType('text/plain', 'attachment'), - $this->getMockedPartWithContentType('text/html', 'attachment'), + $this->getMockedPartWithContentType('text/html', 'inline', true), + $this->getMockedPartWithContentType('text/plain', 'attachment', true), + $this->getMockedPartWithContentType('text/html', 'attachment', true), $this->getMockedPartWithContentType('multipart/relative'), $signedPartParent, $signedPart diff --git a/tests/MailMimeParser/Message/UUEncodedPartTest.php b/tests/MailMimeParser/Message/UUEncodedPartTest.php deleted file mode 100644 index 0a4e3842..00000000 --- a/tests/MailMimeParser/Message/UUEncodedPartTest.php +++ /dev/null @@ -1,42 +0,0 @@ -getMockBuilder('ZBateson\MailMimeParser\Message\Writer\MimePartWriter') - ->disableOriginalConstructor() - ->getMock(); - - $part = new UUEncodedPart($hf, $pw, 0754, 'test-file.ext'); - $this->assertNotNull($part); - - $this->assertEquals('application/octet-stream', $part->getHeaderValue('Content-Type')); - $this->assertEquals('test-file.ext', $part->getHeaderParameter('Content-Type', 'name')); - $this->assertEquals('x-uuencode', $part->getHeaderValue('Content-Transfer-Encoding')); - $this->assertEquals('attachment', $part->getHeaderValue('Content-Disposition')); - $this->assertEquals('test-file.ext', $part->getHeaderParameter('Content-Disposition', 'filename')); - $this->assertEquals('test-file.ext', $part->getFilename()); - $this->assertEquals(0754, $part->getUnixFileMode()); - } -} diff --git a/tests/MailMimeParser/MessageTest.php b/tests/MailMimeParser/MessageTest.php index 59965ece..44cded59 100644 --- a/tests/MailMimeParser/MessageTest.php +++ b/tests/MailMimeParser/MessageTest.php @@ -2,189 +2,404 @@ namespace ZBateson\MailMimeParser; use PHPUnit_Framework_TestCase; +use GuzzleHttp\Psr7; +use org\bovigo\vfs\vfsStream; /** * Description of MessageTest * - * @group Message + * @group MessageClass * @group Base * @covers ZBateson\MailMimeParser\Message * @author Zaahid Bateson */ class MessageTest extends PHPUnit_Framework_TestCase { - protected function getMockedPart() + private $mockPartStreamFilterManager; + private $mockHeaderFactory; + private $mockPartFilterFactory; + private $mockStreamFactory; + private $mockMessageHelperService; + private $vfs; + + protected function setUp() { - $part = $this->getMockBuilder('ZBateson\MailMimeParser\Message\MimePart') + $this->vfs = vfsStream::setup('root'); + $this->mockPartStreamFilterManager = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager') + ->disableOriginalConstructor() + ->getMock(); + $this->mockHeaderFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Header\HeaderFactory') + ->disableOriginalConstructor() + ->getMock(); + $this->mockPartFilterFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilterFactory') + ->disableOriginalConstructor() + ->getMock(); + $this->mockStreamFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Stream\StreamFactory') + ->disableOriginalConstructor() + ->getMock(); + $this->mockMessageHelperService = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Helper\MessageHelperService') ->disableOriginalConstructor() - ->setMethods(['setRawHeader', 'getHeader', 'getHeaderValue', 'getHeaderParameter', 'getContentResourceHandle']) ->getMock(); - return $part; } - protected function getMockedMessageWriter() + protected function getMockedParameterHeader($name, $value, $parameterValue = null) { - $mw = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Writer\MessageWriter') + $header = $this->getMockBuilder('ZBateson\MailMimeParser\Header\ParameterHeader') ->disableOriginalConstructor() + ->setMethods(['getValue', 'getName', 'getValueFor', 'hasParameter']) ->getMock(); - return $mw; + $header->method('getName')->willReturn($name); + $header->method('getValue')->willReturn($value); + $header->method('getValueFor')->willReturn($parameterValue); + $header->method('hasParameter')->willReturn(true); + return $header; } - protected function getMockedHeaderFactory() + protected function getMockedPartBuilder() { - $headerFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Header\HeaderFactory') + return $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\PartBuilder') ->disableOriginalConstructor() ->getMock(); - return $headerFactory; } - protected function getMockedPartFactory() + protected function getMockedPartBuilderWithChildren() { - $partFactory = $this->getMockBuilder('ZBateson\MailMimeParser\Message\MimePartFactory') + $pb = $this->getMockedPartBuilder(); + $children = [ + $this->getMockedPartBuilder(), + $this->getMockedPartBuilder(), + $this->getMockedPartBuilder() + ]; + + $nestedMimePart = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\MimePart') + ->disableOriginalConstructor() + ->getMock(); + + $nested = $this->getMockedPartBuilder(); + $nested->method('createMessagePart') + ->willReturn($nestedMimePart); + $children[0]->method('getChildren') + ->willReturn([$nested]); + + foreach ($children as $key => $child) { + // need to 'setMethods' because getAllNonFilteredParts is protected + $childMimePart = $this->getMockBuilder('ZBateson\MailMimeParser\Message\Part\MimePart') ->disableOriginalConstructor() + ->setMethods([ + 'getAllNonFilteredParts', + '__destruct', + 'getContentResourceHandle', + 'getContent', + 'getStream', + 'isTextPart', + 'getHeaderValue' + ]) ->getMock(); - return $partFactory; + $childMimePart-> + method('getMessageObjectId') + ->willReturn('child' . $key); + + if ($key === 0) { + $childMimePart->expects($this->any()) + ->method('getAllNonFilteredParts') + ->willReturn([$childMimePart, $nestedMimePart]); + } else { + $childMimePart + ->method('getAllNonFilteredParts') + ->willReturn([$childMimePart]); + } + + $child->method('createMessagePart') + ->willReturn($childMimePart); + } + $pb->method('getChildren') + ->willReturn($children); + return $pb; } - protected function createNewMessage($contentType = null) + public function testInstance() { - $hf = $this->getMockedHeaderFactory(); - $mw = $this->getMockedMessageWriter(); - $pf = $this->getMockedPartFactory(); - $message = $this->getMockBuilder('ZBateson\MailMimeParser\Message') - ->setConstructorArgs([$hf, $mw, $pf]) - ->setMethods(['getHeaderValue']) - ->getMock(); - $message->method('getHeaderValue')->will($this->returnCallback(function($param, $defaultValue = null) use ($contentType) { - if (strcasecmp($param, 'Content-Type') === 0 && $contentType !== null) { - return $contentType; - } - return $defaultValue; - })); - return $message; + $message = new Message( + $this->mockPartStreamFilterManager, + $this->mockStreamFactory, + $this->mockPartFilterFactory, + $this->mockHeaderFactory, + $this->getMockedPartBuilder(), + $this->mockMessageHelperService, + Psr7\stream_for('habibis'), + Psr7\stream_for('7ajat 7ilwa') + ); + $this->assertNotNull($message); + $this->assertInstanceOf('ZBateson\MailMimeParser\Message', $message); } - - public function testObjectId() + + public function testGetTextPartAndTextPartCount() { - $message = $this->createNewMessage(); - $message2 = $this->createNewMessage(); - $this->assertNotEmpty($message->getObjectId()); - $this->assertSame($message->getObjectId(), $message->getObjectId()); - $this->assertSame($message2->getObjectId(), $message2->getObjectId()); - $this->assertNotSame($message->getObjectId(), $message2->getObjectId()); + $filterMock = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilter') + ->disableOriginalConstructor() + ->setMethods(['filter']) + ->getMock(); + $filterMock + ->method('filter') + ->willReturnOnConsecutiveCalls( + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false + ); + $this->mockPartFilterFactory + ->method('newFilterFromInlineContentType') + ->willReturn($filterMock); + + $message = new Message( + $this->mockPartStreamFilterManager, + $this->mockStreamFactory, + $this->mockPartFilterFactory, + $this->mockHeaderFactory, + $this->getMockedPartBuilderWithChildren(), + $this->mockMessageHelperService, + Psr7\stream_for('habibis'), + Psr7\stream_for('7ajat 7ilwa') + ); + + $parts = $message->getAllParts(); + $parts[1]->method('getContentResourceHandle') + ->willReturn('oufa baloufa!'); + $parts[1]->method('getContent') + ->willReturn('shabadabada...'); + + $this->assertEquals(2, $message->getTextPartCount()); + $this->assertEquals($parts[1], $message->getTextPart()); + $this->assertEquals($parts[3], $message->getTextPart(1)); + $this->assertNull($message->getTextPart(2)); + $this->assertNull($message->getTextStream(2)); + $this->assertNull($message->getTextContent(2)); + $this->assertEquals('oufa baloufa!', $message->getTextStream()); + $this->assertEquals('shabadabada...', $message->getTextContent()); } - public function testAddHtmlPart() + public function testGetHtmlPartAndHtmlPartCount() { - $part = $this->getMockedPart(); - $part->method('getHeaderValue')->will($this->returnCallback(function($param, $defaultValue = null) { - if (strcasecmp($param, 'Content-Type') === 0) { - return 'text/html'; - } - return $defaultValue; - })); - $part->method('getContentResourceHandle')->willReturn('handle'); - - $message = $this->createNewMessage('multipart/alternative'); - $message->addPart($part); + $filterMock = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilter') + ->disableOriginalConstructor() + ->setMethods(['filter']) + ->getMock(); + $filterMock + ->method('filter') + ->willReturnOnConsecutiveCalls( + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false + ); + $this->mockPartFilterFactory + ->method('newFilterFromInlineContentType') + ->willReturn($filterMock); - $this->assertNull($message->getTextPart()); - $this->assertNull($message->getAttachmentPart(0)); - $this->assertSame($part, $message->getPartByMimeType('text/html')); - $this->assertSame($part, $message->getHtmlPart()); - $this->assertEquals('handle', $message->getHtmlStream()); - $this->assertNull($message->getTextStream()); + $message = new Message( + $this->mockPartStreamFilterManager, + $this->mockStreamFactory, + $this->mockPartFilterFactory, + $this->mockHeaderFactory, + $this->getMockedPartBuilderWithChildren(), + $this->mockMessageHelperService, + Psr7\stream_for('habibis'), + Psr7\stream_for('7ajat 7ilwa') + ); + + $parts = $message->getAllParts(); + $parts[1]->method('getContentResourceHandle') + ->willReturn('oufa baloufa!'); + $parts[1]->method('getContent') + ->willReturn('shabadabada...'); + + $this->assertEquals(2, $message->getHtmlPartCount()); + $this->assertEquals($parts[1], $message->getHtmlPart()); + $this->assertEquals($parts[3], $message->getHtmlPart(1)); + $this->assertNull($message->getHtmlPart(2)); + $this->assertNull($message->getHtmlStream(2)); + $this->assertNull($message->getHtmlContent(2)); + $this->assertEquals('oufa baloufa!', $message->getHtmlStream()); + $this->assertEquals('shabadabada...', $message->getHtmlContent()); } - - public function testAddTextPart() + + public function testGetAttachmentParts() { - $part = $this->getMockedPart(); - $part->method('getHeaderValue')->will($this->returnCallback(function($param, $defaultValue = null) { - if ($param === 'Content-Type') { - return 'text/plain'; - } - return $defaultValue; - })); - $part->method('getContentResourceHandle')->willReturn('handle'); + $filterMock = $this->getMockBuilder('ZBateson\MailMimeParser\Message\PartFilter') + ->disableOriginalConstructor() + ->setMethods(['filter']) + ->getMock(); + $filterMock + ->method('filter') + ->willReturnOnConsecutiveCalls( + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false, + false, true, false, true, false + ); + $this->mockPartFilterFactory + ->method('newFilterFromArray') + ->willReturn($filterMock); + + $message = new Message( + $this->mockPartStreamFilterManager, + $this->mockStreamFactory, + $this->mockPartFilterFactory, + $this->mockHeaderFactory, + $this->getMockedPartBuilderWithChildren(), + $this->mockMessageHelperService, + Psr7\stream_for('habibis'), + Psr7\stream_for('7ajat 7ilwa') + ); + + $parts = $message->getAllParts(); + $parts[1]->method('isTextPart') + ->willReturn(true); + $parts[1]->method('getHeaderValue') + ->with('Content-Disposition', 'inline') + ->willReturn('attachment'); + $parts[3]->method('isTextPart') + ->willReturn(true); + $parts[3]->method('getHeaderValue') + ->with('Content-Disposition', 'inline') + ->willReturn('inline'); - $message = $this->createNewMessage('multipart/alternative'); - $message->addPart($part); - $this->assertNull($message->getHtmlPart()); - $this->assertNull($message->getAttachmentPart(0)); - $this->assertSame($part, $message->getPartByMimeType('text/plain')); - $this->assertSame($part, $message->getTextPart()); - $this->assertEquals('handle', $message->getTextStream()); - $this->assertNull($message->getHtmlStream()); + $this->assertEquals(1, $message->getAttachmentCount()); + $this->assertEquals([$parts[1]], $message->getAllAttachmentParts()); + $this->assertEquals($parts[1], $message->getAttachmentPart(0)); + $this->assertNull($message->getAttachmentPart(1)); } - - public function testAddAttachmentPart() + + public function testIsNotMime() { - $part = $this->getMockedPart(); - $part->method('getHeaderValue')->will($this->returnCallback(function($param, $defaultValue = null) { - if ($param === 'Content-Type') { - return 'image/png'; - } elseif ($param === 'Content-Disposition') { - return 'attachment'; - } - return $defaultValue; - })); + $message = new Message( + $this->mockPartStreamFilterManager, + $this->mockStreamFactory, + $this->mockPartFilterFactory, + $this->mockHeaderFactory, + $this->getMockedPartBuilder(), + $this->mockMessageHelperService, + Psr7\stream_for('habibis'), + Psr7\stream_for('7ajat 7ilwa') + ); + $this->assertFalse($message->isMime()); + } + + public function testIsMimeWithContentType() + { + $hf = $this->mockHeaderFactory; + $header = $this->getMockedParameterHeader('Content-Type', 'text/plain', 'utf-8'); + + $pb = $this->getMockedPartBuilder(); + $pb->method('getContentType') + ->willReturn($header); + $pb->method('getRawHeaders') + ->willReturn(['contenttype' => ['Blah', 'Blah']]); - $message = $this->createNewMessage('multipart/mixed'); - $message->addPart($part); - $this->assertNull($message->getHtmlPart()); - $this->assertNull($message->getTextPart()); - $this->assertEquals(1, $message->getAttachmentCount()); - $this->assertSame($part, $message->getAttachmentPart(0)); - $this->assertEquals([$part], $message->getAllAttachmentParts()); + $message = new Message( + $this->mockPartStreamFilterManager, + $this->mockStreamFactory, + $this->mockPartFilterFactory, + $hf, + $pb, + $this->mockMessageHelperService, + Psr7\stream_for('habibis'), + Psr7\stream_for('7ajat 7ilwa') + ); + $this->assertTrue($message->isMime()); } - public function testGetParts() + public function testIsMimeWithMimeVersion() { - $part = $this->getMockedPart(); - $part->method('getHeaderValue')->will($this->returnCallback(function($param, $defaultValue = null) { - if ($param === 'Content-Type') { - return 'image/png'; - } else if ($param === 'Content-Disposition') { - return 'attachment'; - } - return $defaultValue; - })); + $hf = $this->mockHeaderFactory; + $header = $this->getMockedParameterHeader('Mime-Version', '4.3'); + $hf->method('newInstance') + ->willReturn($header); - $part2 = $this->getMockedPart(); - $part2->method('getHeaderValue')->will($this->returnCallback(function($param, $defaultValue = null) { - if ($param === 'Content-Type') { - return 'text/html'; - } - return $defaultValue; - })); + $pb = $this->getMockedPartBuilder(); + $pb->method('getRawHeaders') + ->willReturn(['mimeversion' => ['Mime-Version', '4.3']]); - $message = $this->createNewMessage('multipart/mixed'); - $message->addPart($part); - $message->addPart($part2); - $this->assertNull($message->getTextPart()); - $this->assertSame($part2, $message->getHtmlPart()); - $this->assertEquals(3, $message->getPartCount()); - $this->assertSame($message, $message->getPart(0)); - $this->assertSame($part, $message->getPart(1)); - $this->assertSame($part2, $message->getPart(2)); - $this->assertSame($part, $message->getChild(0)); - $this->assertSame($part2, $message->getChild(1)); - $this->assertEquals([$message, $part, $part2], $message->getAllParts()); + $message = new Message( + $this->mockPartStreamFilterManager, + $this->mockStreamFactory, + $this->mockPartFilterFactory, + $hf, + $pb, + $this->mockMessageHelperService, + Psr7\stream_for('habibis'), + Psr7\stream_for('7ajat 7ilwa') + ); + $this->assertTrue($message->isMime()); } - public function testMessageIsMime() + public function testSaveAndToString() { - $message = $this->createNewMessage(); - $this->assertFalse($message->isMime()); + $content = vfsStream::newFile('part')->at($this->vfs); + $content->withContent('Demigorgon'); + $messageHandle = fopen($content->url(), 'r'); + + $pb = $this->getMockedPartBuilder(); + $pb->method('getStreamContentLength')->willReturn(0); + $message = new Message( + $this->mockPartStreamFilterManager, + $this->mockStreamFactory, + $this->mockPartFilterFactory, + $this->mockHeaderFactory, + $pb, + $this->mockMessageHelperService, + Psr7\stream_for($messageHandle), + Psr7\stream_for('7ajat 7ilwa') + ); + + $handle = fopen('php://temp', 'r+'); + $message->save($handle); + rewind($handle); + $str = stream_get_contents($handle); + fclose($handle); + + $this->assertEquals('Demigorgon', $str); + $this->assertEquals('Demigorgon', $message->__toString()); + } + + public function testGetSignedMessageAsStringWithoutChildren() + { + $message = new Message( + $this->mockPartStreamFilterManager, + $this->mockStreamFactory, + $this->mockPartFilterFactory, + $this->mockHeaderFactory, + $this->getMockedPartBuilder(), + $this->mockMessageHelperService, + Psr7\stream_for('habibis'), + Psr7\stream_for('7ajat 7ilwa') + ); + $this->assertNull($message->getSignedMessageAsString()); } - public function testGetTextPartFromMessageWithoutContentType() + public function testGetSignedMessageAsString() { - $message = $this->createNewMessage(); - $message->setContent('Test'); + $message = new Message( + $this->mockPartStreamFilterManager, + $this->mockStreamFactory, + $this->mockPartFilterFactory, + $this->mockHeaderFactory, + $this->getMockedPartBuilderWithChildren(), + $this->mockMessageHelperService, + Psr7\stream_for('habibis'), + Psr7\stream_for('7ajat 7ilwa') + ); + $child = $message->getChild(0); - $textPart = $message->getTextPart(); - $this->assertNotNull($textPart); - $this->assertSame($message, $textPart); + $child->expects($this->once())->method('getStream')->willReturn(Psr7\stream_for('Much success')); + $this->assertEquals('Much success', $message->getSignedMessageAsString()); } } diff --git a/tests/MailMimeParser/SimpleDiTest.php b/tests/MailMimeParser/SimpleDiTest.php index 4f0eac5d..272093f7 100644 --- a/tests/MailMimeParser/SimpleDiTest.php +++ b/tests/MailMimeParser/SimpleDiTest.php @@ -6,7 +6,7 @@ /** * Description of SimpleDiTest * - * @group SimpleDiTest + * @group SimpleDi * @group Base * @covers ZBateson\MailMimeParser\SimpleDi * @author Zaahid Bateson @@ -28,28 +28,13 @@ public function testNewMessageParser() $this->assertNotNull($mp); } - public function testNewMessage() + public function testGetCharsetConverter() { $di = SimpleDi::singleton(); - $m = $di->newMessage(); + $m = $di->getCharsetConverter('ISO-8859-1', 'UTF-8'); $this->assertNotNull($m); } - public function testNewCharsetConverter() - { - $di = SimpleDi::singleton(); - $m = $di->newCharsetConverter('ISO-8859-1', 'UTF-8'); - $this->assertNotNull($m); - } - - public function testGetPartFactory() - { - $di = SimpleDi::singleton(); - $singleton = $di->getPartFactory(); - $this->assertNotNull($singleton); - $this->assertSame($singleton, $di->getPartFactory()); - } - public function testGetHeaderFactory() { $di = SimpleDi::singleton(); @@ -66,14 +51,6 @@ public function testGetHeaderPartFactory() $this->assertSame($singleton, $di->getHeaderPartFactory()); } - public function testGetPartStreamRegistry() - { - $di = SimpleDi::singleton(); - $singleton = $di->getPartStreamRegistry(); - $this->assertNotNull($singleton); - $this->assertSame($singleton, $di->getPartStreamRegistry()); - } - public function testGetMimeLiteralPartFactory() { $di = SimpleDi::singleton(); diff --git a/tests/MailMimeParser/Stream/Helper/CharsetConverterTest.php b/tests/MailMimeParser/Stream/Helper/CharsetConverterTest.php deleted file mode 100644 index 50caeddd..00000000 --- a/tests/MailMimeParser/Stream/Helper/CharsetConverterTest.php +++ /dev/null @@ -1,58 +0,0 @@ -assertEquals($test, $convertBack->convert($convert->convert($test)), "Testing with $dest"); - } - } - - public function testIconvCharsetConversion() - { - $arr = array_unique(CharsetConverter::$iconvAliases); - $test = 'This is my string'; - foreach ($arr as $dest) { - $convert = new CharsetConverter('UTF-8', $dest); - $convertBack = new CharsetConverter($dest, 'utf-8'); - $this->assertEquals($test, $convertBack->convert($convert->convert($test)), "Testing with $dest"); - } - } - - public function testSetCharsetConversions() - { - $arr = [ - 'ISO-8859-8-I', - 'WINDOWS-1254', - 'CSPC-850-MULTILINGUAL', - 'GB18030_2000', - 'ISO_IR_157', - 'CS-ISO-LATIN-4', - 'ISO_IR_100', - ]; - $test = 'This is my string'; - - foreach ($arr as $dest) { - $convert = new CharsetConverter('UTF-8', $dest); - $convertBack = new CharsetConverter($dest, 'utf-8'); - $this->assertEquals($test, $convertBack->convert($convert->convert($test)), "Testing with $dest"); - } - } -} diff --git a/tests/MailMimeParser/Stream/PartStreamRegistryTest.php b/tests/MailMimeParser/Stream/PartStreamRegistryTest.php deleted file mode 100644 index 63853bc9..00000000 --- a/tests/MailMimeParser/Stream/PartStreamRegistryTest.php +++ /dev/null @@ -1,70 +0,0 @@ -registry = $di->getPartStreamRegistry(); - } - - public function testRegisteringAndUnregistering() - { - $mem = fopen('php://memory', 'rw'); - fwrite($mem, 'This is a test'); - $mem2 = fopen('php://memory', 'rw'); - fwrite($mem2, 'This is a test'); - $mem3 = fopen('php://memory', 'rw'); - fwrite($mem3, 'This is a test'); - - $this->registry->register(1, $mem); - $this->registry->register(2, $mem2); - $this->registry->register(3, $mem3); - - $ps = @fopen('mmp-mime-message://1?start=1&end=4', 'r'); - $ps2 = @fopen('mmp-mime-message://2?start=1&end=4', 'r'); - $ps3 = @fopen('mmp-mime-message://3?start=1&end=4', 'r'); - - $this->assertNotNull($ps); - $this->assertNotNull($ps2); - $this->assertNotNull($ps3); - - $this->assertSame($mem, $this->registry->get(1)); - $this->assertSame($mem2, $this->registry->get(2)); - $this->assertSame($mem3, $this->registry->get(3)); - - $this->registry->increaseHandleRefCount(1); - $this->assertSame($mem, $this->registry->get(1)); - $this->registry->decreaseHandleRefCount(1); - $this->assertSame($mem, $this->registry->get(1)); - $this->registry->decreaseHandleRefCount(1); - $this->assertNull($this->registry->get(1)); - - fclose($ps); - fclose($ps2); - fclose($ps3); - - $ps2 = @fopen('mmp-mime-message://2?start=1&end=4', 'r'); - $this->assertFalse($ps2); - - $ps = @fopen('mmp-mime-message://1?start=1&end=4', 'r'); - $this->assertFalse($ps); - - $ps3 = @fopen('mmp-mime-message://3?start=1&end=4', 'r'); - $this->assertFalse($ps3); - } -} diff --git a/tests/MailMimeParser/Stream/PartStreamTest.php b/tests/MailMimeParser/Stream/PartStreamTest.php deleted file mode 100644 index e947e060..00000000 --- a/tests/MailMimeParser/Stream/PartStreamTest.php +++ /dev/null @@ -1,112 +0,0 @@ -di = SimpleDi::singleton(); - $this->registry = $this->di->getPartStreamRegistry(); - } - - public function testReadLimits() - { - $mem = fopen('php://memory', 'rw'); - fwrite($mem, 'This is a test'); - $this->registry->register('testReadLimits', $mem); - - $res = fopen('mmp-mime-message://testReadLimits?start=1&end=4', 'r'); - $this->assertNotNull($res); - $str = stream_get_contents($res); - $this->assertEquals('his', $str); - - fclose($res); - } - - public function testReadLimitsToEnd() - { - $mem = fopen('php://memory', 'rw'); - fwrite($mem, 'test'); - $this->registry->register('testReadLimitsToEnd', $mem); - - $res = fopen('mmp-mime-message://testReadLimitsToEnd?start=0&end=4', 'r'); - $this->assertNotNull($res); - $str = stream_get_contents($res); - $this->assertEquals('test', $str); - - fclose($res); - } - - public function testPosition() - { - $mem = fopen('php://memory', 'rw'); - fwrite($mem, 'This is a test'); - $this->registry->register('testReadLimits', $mem); - - $res = fopen('mmp-mime-message://testReadLimits?start=1&end=4', 'r'); - $this->assertNotNull($res); - $this->assertEquals(0, ftell($res)); - $str = stream_get_contents($res); - $this->assertEquals(3, ftell($res)); - - fclose($res); - } - - public function testEof() - { - $mem = fopen('php://memory', 'rw'); - fwrite($mem, 'This is a test'); - $this->registry->register('testReadLimits', $mem); - - $res = fopen('mmp-mime-message://testReadLimits?start=1&end=4', 'r'); - $this->assertNotNull($res); - $this->assertFalse(feof($res)); - $str = stream_get_contents($res); - $this->assertTrue(feof($res)); - - fclose($res); - } - - public function testSeek() - { - $mem = fopen('php://memory', 'rw'); - fwrite($mem, 'This is a test'); - $this->registry->register('testReadLimits', $mem); - - $res = fopen('mmp-mime-message://testReadLimits?start=1&end=4', 'r'); - $this->assertNotNull($res); - - $this->assertEquals(-1, fseek($res, -1, SEEK_SET)); - $this->assertEquals(-1, fseek($res, 4, SEEK_SET)); - $this->assertEquals(-1, fseek($res, 1, SEEK_END)); - $this->assertEquals(-1, fseek($res, -1, SEEK_CUR)); - - $this->assertEquals(0, fseek($res, 2, SEEK_SET)); - $str = stream_get_contents($res); - $this->assertEquals('s', $str); - - $this->assertEquals(0, fseek($res, -2, SEEK_CUR)); - $str = stream_get_contents($res); - $this->assertEquals('is', $str); - - $this->assertEquals(0, fseek($res, -1, SEEK_END)); - $str = stream_get_contents($res); - $this->assertEquals('s', $str); - - fclose($res); - } -} diff --git a/tests/_data/emails/m4008.txt b/tests/_data/emails/m4008.txt index b82e81c2..5465d7f1 100644 --- a/tests/_data/emails/m4008.txt +++ b/tests/_data/emails/m4008.txt @@ -1,44 +1,224 @@ -From: "Doug Sauder" -To: "Jürgen Schmürgen" -Subject: Die Hasen und die Frösche (Microsoft Outlook 00) -Date: Wed, 17 May 2000 19:08:29 -0400 -Message-ID: -MIME-Version: 1.0 -Content-Type: multipart/signed; - boundary="----=MMP-57d495ede8684.57d495edeb0d04.56408213"; - micalg="pgp-sha256"; protocol="application/x-pgp-signature" -X-Priority: 3 (Normal) -X-MSMail-Priority: Normal -X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) -Importance: Normal -X-MimeOLE: Produced By Microsoft MimeOLE V5.00.2314.1300 - - -------=MMP-57d495ede8684.57d495edeb0d04.56408213 -Content-Type: text/plain; - charset="iso-8859-1" -Content-Transfer-Encoding: 8bit - -Die Hasen und die Frösche - -Die Hasen klagten einst über ihre mißliche Lage; "wir leben", sprach ein -Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, der -Adler, ja fast aller Raubtiere! Unsere stete Angst ist ärger als der Tod -selbst. Auf, laßt uns ein für allemal sterben." - -In einem nahen Teich wollten sie sich nun ersäufen; sie eilten ihm zu; -allein das außerordentliche Getöse und ihre wunderbare Gestalt erschreckte -eine Menge Frösche, die am Ufer saßen, so sehr, daß sie aufs schnellste -untertauchten. - -"Halt", rief nun eben dieser Sprecher, "wir wollen das Ersäufen noch ein -wenig aufschieben, denn auch uns fürchten, wie ihr seht, einige Tiere, -welche also wohl noch unglücklicher sein müssen als wir." - - -------=MMP-57d495ede8684.57d495edeb0d04.56408213 -Content-Type: application/pgp-signature - -9825cba003a7ac85b9a3f3dc9f8423fd -------=MMP-57d495ede8684.57d495edeb0d04.56408213-- - +Message-ID: <39235FC5.276CCE00@example.com> +Date: Wed, 17 May 2000 23:13:09 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Heinz =?iso-8859-1?Q?M=FCller?= +Subject: Die Hasen und die =?iso-8859-1?Q?Fr=F6sche?= (Netscape Messenger 4.7) +Content-Type: multipart/signed; + boundary="----=MMP-57d495ede8684.57d495edeb0d04.56408213"; + micalg="pgp-sha256"; protocol="application/x-pgp-signature" + +------=MMP-57d495ede8684.57d495edeb0d04.56408213 +Content-Type: multipart/mixed; + boundary="------------A1E83A41894D3755390B838A" + +This is a multi-part message in MIME format. +--------------A1E83A41894D3755390B838A +Content-Type: multipart/alternative; + boundary="------------F03F94BA73D3B9E8C1B94D92" + + +--------------F03F94BA73D3B9E8C1B94D92 +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +[blue ball] + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, +der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als de= +r +Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben." + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs +schnellste untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ei= +n +wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere, +welche also wohl noch ungl=FCcklicher sein m=FCssen als wir." + +[Image] + + + +--------------F03F94BA73D3B9E8C1B94D92 +Content-Type: multipart/related; + boundary="------------C02FA3D0A04E95F295FB25EB" + + +--------------C02FA3D0A04E95F295FB25EB +Content-Type: text/html; charset=us-ascii +Content-Transfer-Encoding: 7bit + + + +blue ball +

Die Hasen und die Frösche +

Die Hasen klagten einst über ihre mißliche Lage; "wir leben", +sprach ein Redner, "in steter Furcht vor Menschen und Tieren, eine Beute +der Hunde, der Adler, ja fast aller Raubtiere! Unsere stete Angst ist ärger +als der Tod selbst. Auf, laßt uns ein für allemal sterben." +

In einem nahen Teich wollten sie sich nun ersäufen; sie eilten +ihm zu; allein das außerordentliche Getöse und ihre wunderbare +Gestalt erschreckte eine Menge Frösche, die am Ufer saßen, so +sehr, daß sie aufs schnellste untertauchten. +

"Halt", rief nun eben dieser Sprecher, "wir wollen das Ersäufen +noch ein wenig aufschieben, denn auch uns fürchten, wie ihr seht, +einige Tiere, welche also wohl noch unglücklicher sein müssen +als wir." +

+
  +
  + +--------------C02FA3D0A04E95F295FB25EB +Content-Type: image/png +Content-ID: +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="C:\TEMP\nsmailEG.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgA +AAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQ +MZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYY +QsYQMaUAACHO5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9K +e+8YOaUYSsaMvee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB +Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAGI +SURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbsscebL5xznTsh +5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqW +Uw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M +jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1C +SYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIom +H3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0N +xW62p+lT+Yi747sD/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBi +eSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--------------C02FA3D0A04E95F295FB25EB +Content-Type: image/png +Content-ID: +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="C:\TEMP\nsmail39.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAV +AAAaAAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACH +AAB9AAB0AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABb +AAAuAAAIAABMAAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACa +AAC7JCTRYWHfhITmf3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5Pl +rKzpmZntZWXvJSXXAADBAACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzr +pqbtiorvUVHvFBTRAADDAAC2AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjv +V1fvJibhAADOAAC3AACnAACVAABHAAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQ +AADJAAC1AACXAACEAABsAABPAAASAAACAABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAAT +AAAkAABYAADIAADTAADNAACzAACDAABuAAAeAAB+AADAAACkAACNAAB/AABpAABQAAAwAACR +AACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACsAACvAACtAACmAACJAAB6AABrAABaAAA+ +AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABVAACOAACKAAA4AAAQAAA/AAByAACA +AABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAMAAAdAAANAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8 +LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAII +SURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iKiUtI8koJ +Scsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ29ja +2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2Yn +OAj+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/ +uXLzVJ2qm6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW +0g0bN63crGqVtWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36Kw +bNmRo7O3zpHkPSZwHBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8 +YVOlI+CJ4/9/joOyYed5QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms +1y9evXid7QZacgOxmSxktNzdtSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21t +ZW50AGNsaXAyZ2lmIHYuMC42IGJ5IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--------------C02FA3D0A04E95F295FB25EB-- + +--------------F03F94BA73D3B9E8C1B94D92-- + +--------------A1E83A41894D3755390B838A +Content-Type: image/png; + name="redball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAV +AAAaAAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACH +AAB9AAB0AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABb +AAAuAAAIAABMAAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACa +AAC7JCTRYWHfhITmf3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5Pl +rKzpmZntZWXvJSXXAADBAACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzr +pqbtiorvUVHvFBTRAADDAAC2AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjv +V1fvJibhAADOAAC3AACnAACVAABHAAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQ +AADJAAC1AACXAACEAABsAABPAAASAAACAABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAAT +AAAkAABYAADIAADTAADNAACzAACDAABuAAAeAAB+AADAAACkAACNAAB/AABpAABQAAAwAACR +AACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACsAACvAACtAACmAACJAAB6AABrAABaAAA+ +AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABVAACOAACKAAA4AAAQAAA/AAByAACA +AABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAMAAAdAAANAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8 +LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAII +SURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iKiUtI8koJ +Scsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ29ja +2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2Yn +OAj+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/ +uXLzVJ2qm6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW +0g0bN63crGqVtWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36Kw +bNmRo7O3zpHkPSZwHBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8 +YVOlI+CJ4/9/joOyYed5QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms +1y9evXid7QZacgOxmSxktNzdtSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21t +ZW50AGNsaXAyZ2lmIHYuMC42IGJ5IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--------------A1E83A41894D3755390B838A +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAA +IQAACAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAh +rQBCvRhjxjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBC +zhDO55Te563G55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY +1gAx5wBS7yFr7zlK7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp +1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAFt +SURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy+N9ery1b +Ve9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh0fHJ +aTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJV +G9EaEjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyex +I0ZxBP3afE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWr +YUO23hMANUKRRl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0 +RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5C +YII= +--------------A1E83A41894D3755390B838A-- +------=MMP-57d495ede8684.57d495edeb0d04.56408213 +Content-Type: application/pgp-signature + +9f5c560f86b607c9087b84e9baa98189 + +------=MMP-57d495ede8684.57d495edeb0d04.56408213-- + + diff --git a/tests/phpunit.xml b/tests/phpunit.xml index ef85e6f9..80c0dd01 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -30,12 +30,7 @@ MailMimeParser/MessageTest.php - MailMimeParser/Message/MessageParserTest.php - MailMimeParser/Message/MimePartFactoryTest.php - MailMimeParser/Message/MimePartTest.php - MailMimeParser/Message/NonMimePartTest.php - MailMimeParser/Message/PartFilterTest.php - MailMimeParser/Message/UUEncodedPartTest.php + MailMimeParser/Message MailMimeParser/Header @@ -43,6 +38,9 @@ MailMimeParser/Stream + + MailMimeParser/Util + MailMimeParser/IntegrationTests diff --git a/version.txt b/version.txt index e8423da8..3eefcb9d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.4.10 +1.0.0