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
+
+
+
+
+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