├── .editorconfig ├── .gitignore ├── .php_cs ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml ├── sample ├── multipart.eml ├── plain.eml └── rfc822.eml ├── src ├── Message │ ├── Header.php │ ├── HeaderInterface.php │ ├── MessagePart.php │ └── MessagePartInterface.php ├── MessageParser.php └── MessageParserInterface.php └── tests └── MessageParserTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.eml] 12 | end_of_line = crlf 13 | 14 | [*.json] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | build 3 | composer.lock 4 | .php_cs.cache 5 | nbproject -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | 7 | 8 | This source file is subject to the MIT license that is bundled 9 | with this source code in the file LICENSE.md. 10 | EOF; 11 | 12 | use Symfony\CS\Config\Config; 13 | use Symfony\CS\Finder\DefaultFinder; 14 | use Symfony\CS\FixerInterface; 15 | use Symfony\CS\Fixer\Contrib\HeaderCommentFixer; 16 | 17 | HeaderCommentFixer::setHeader($header); 18 | 19 | return Config::create() 20 | ->finder( 21 | DefaultFinder::create() 22 | ->in(__DIR__ . '/src') 23 | ->in(__DIR__ . '/tests') 24 | ) 25 | ->fixers(array( 26 | 'header_comment', 27 | 'long_array_syntax' 28 | )) 29 | ->level(FixerInterface::PSR2_LEVEL) 30 | ->setUsingCache(true); 31 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | php: 3 | code_rating: true 4 | duplication: true 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | after_success: 2 | - travis_retry php vendor/bin/coveralls -x build/logs/coverage.xml 3 | - export CODECLIMATE_REPO_TOKEN=3e36b965e9e120d11a3c822f75e5f8b1c00533bf3b4615861e9774807d1440eb 4 | - travis_retry php vendor/bin/test-reporter --coverage-report=build/logs/coverage.xml 5 | 6 | before_install: 7 | - travis_retry composer self-update 8 | 9 | branches: 10 | only: 11 | - master 12 | 13 | cache: 14 | directories: 15 | - $HOME/.composer/cache 16 | 17 | install: 18 | - travis_retry composer install --dev --no-interaction --prefer-dist 19 | 20 | language: php 21 | 22 | php: 23 | - 5.4 24 | - 5.5 25 | - 5.6 26 | - 7.0 27 | - 7.2 28 | 29 | script: 30 | - mkdir -p build/logs 31 | - vendor/bin/phpunit 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Vaibhav Pandey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vaibhavpandeyvpz/phemail 2 | MIME parser written in pure [PHP](http://www.php.net/) for parsing raw emails (.eml) files. 3 | 4 | [![Latest Version](https://img.shields.io/github/release/vaibhavpandeyvpz/phemail.svg?style=flat-square)](https://github.com/vaibhavpandeyvpz/phemail/releases) 5 | [![Coverage Status](https://coveralls.io/repos/github/vaibhavpandeyvpz/phemail/badge.svg?branch=master)](https://coveralls.io/github/vaibhavpandeyvpz/phemail?branch=master) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/vaibhavpandeyvpz/phemail.svg?style=flat-square)](https://packagist.org/packages/vaibhavpandeyvpz/phemail) 7 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 8 | 9 | Install 10 | --- 11 | ```bash 12 | composer require vaibhavpandeyvpz/phemail 13 | ``` 14 | 15 | Usage 16 | --- 17 | Suppose this is your email file named `sample.eml`: 18 | 19 | ```eml 20 | Mime-Version: 1.0 21 | Message-Id: <6B7EC235-5B17-4CA8-B2B8-39290DEB43A3@vaibhavpandey.com> 22 | From: Vaibhav Pandey 23 | To: Vaibhav Pandey 24 | Subject: Testing simple email 25 | Date: Sat, 22 Nov 2008 15:04:59 +1100 26 | Content-Type: text/plain; charset=US-ASCII; format=flowed 27 | Content-Transfer-Encoding: 7bit 28 | 29 | 30 | This is simple as f*** plain text email message. 31 | 32 | Regards, 33 | Vaibhav Pandey 34 | ``` 35 | 36 | You can read & parse it as follows: 37 | 38 | ```php 39 | parse(__DIR__ . '/sample.eml'); 43 | 44 | echo $message->getHeaderValue('subject'); 45 | # outputs 'Testing simple email' 46 | 47 | echo $message->getHeaderValue('date'); 48 | # outputs 'Sat, 22 Nov 2008 15:04:59 +1100' 49 | 50 | echo $message->getHeaderValue('content-type'); 51 | # outputs 'text/plain' 52 | 53 | echo $message->getHeaderAttribute('content-type', 'charset'); 54 | # outputs 'US-ASCII' 55 | 56 | echo $message->getContents(); 57 | 58 | /** 59 | * @desc To extract emails from headers, you could use any RFC 822 60 | * internet address parser e.g., pear/mail. 61 | */ 62 | $addresses = (new Mail_RFC822())->parseAddressList($message->getHeaderValue('to')); 63 | foreach ($addresses as $address) { 64 | echo 'Name: ', $address->personal, '
', 'Email: ', $address->mailbox, '@', $address->host; 65 | } 66 | ``` 67 | 68 | License 69 | ------ 70 | See [LICENSE.md](https://github.com/vaibhavpandeyvpz/phemail/blob/master/LICENSE.md) file. 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoload": { 3 | "psr-4": { 4 | "Phemail\\": "src/" 5 | } 6 | }, 7 | "autoload-dev": { 8 | "psr-4": { 9 | "Phemail\\Tests\\": "tests/" 10 | } 11 | }, 12 | "description": "MIME parser written in pure PHP for parsing raw emails (.eml) files.", 13 | "homepage": "https://github.com/francescogabbrielli/phemail", 14 | "keywords": ["mime", "email", "parser"], 15 | "license": "MIT", 16 | "name": "vaibhavpandeyvpz/phemail", 17 | "require": { 18 | "php": "^5.4 || ^7.0 || ^8.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^4.8" 22 | }, 23 | "suggest": { 24 | "pear/mail": "" 25 | }, 26 | "type": "library" 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | src 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | tests 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sample/multipart.eml: -------------------------------------------------------------------------------- 1 | Mime-Version: 1.0 2 | Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@vaibhavpandey.com> 3 | From: Vaibhav Pandey 4 | To: Vaibhav Pandey 5 | Subject: Testing multipart email 6 | Date: Mon, 6 Jun 2005 22:21:22 +0200 7 | Content-Type: multipart/mixed; boundary="652b8c4dcb00cdcdda1e16af36781caf" 8 | 9 | 10 | --652b8c4dcb00cdcdda1e16af36781caf 11 | Content-Type: text/plain; 12 | charset=US-ASCII; 13 | delsp=yes; 14 | format=flowed 15 | Content-Transfer-Encoding: 7bit 16 | 17 | This is the first part. 18 | The last part contains a ruby file. 19 | 20 | --652b8c4dcb00cdcdda1e16af36781caf 21 | Content-Type: text/html; charset=ISO-8859-1; format=flowed 22 | Content-Transfer-Encoding: quoted-printable 23 | 24 | This is the first part with some HTML formatting. 25 | The last part contains a ruby file. 26 | 27 | --652b8c4dcb00cdcdda1e16af36781caf 28 | Content-Type: text/x-ruby-script; name="hello.rb" 29 | Content-Transfer-Encoding: 7bit 30 | Content-Disposition: attachment; 31 | filename="api.rb" 32 | 33 | puts "Hello, world!" 34 | gets 35 | 36 | --652b8c4dcb00cdcdda1e16af36781caf-- 37 | -------------------------------------------------------------------------------- /sample/plain.eml: -------------------------------------------------------------------------------- 1 | Mime-Version: 1.0 2 | Message-Id: <6B7EC235-5B17-4CA8-B2B8-39290DEB43A3@vaibhavpandey.com> 3 | From: Vaibhav Pandey 4 | To: Vaibhav Pandey 5 | Subject: Testing simple email 6 | Date: Sat, 22 Nov 2008 15:04:59 +1100 7 | Content-Type: text/plain; charset=US-ASCII; format=flowed 8 | Content-Transfer-Encoding: 7bit 9 | 10 | 11 | This is simple as f*** plain text email message. 12 | 13 | Regards, 14 | Vaibhav Pandey 15 | -------------------------------------------------------------------------------- /sample/rfc822.eml: -------------------------------------------------------------------------------- 1 | Subject: Certified Email 2 | Message-ID: 3 | To: certified@registerpec.it 4 | From: posta-certificata@pec-email.com 5 | MIME-Version: 1.0 6 | Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha256; boundary="----------=_1539270648-22547-9" 7 | 8 | ------------=_1539270648-22547-9 9 | MIME-Version: 1.0 10 | Content-Type: multipart/mixed; boundary="----------=_1539270645-22547-8" 11 | 12 | This is a multi-part message in MIME format... 13 | 14 | ------------=_1539270645-22547-8 15 | Content-Type: text/plain; charset="iso-8859-1" 16 | Content-Disposition: inline 17 | Content-Transfer-Encoding: quoted-printable 18 | 19 | Messaggio di posta certificata 20 | Il giorno 11/10/2018 alle ore 17:10:45 (+0200) il messaggio 21 | "Contratti e Documentazione" =E8 stato inviato da "services@pec.services.com" 22 | indirizzato a: 23 | certified@registerpec.it 24 | Il messaggio originale =E8 incluso in allegato. 25 | Identificativo del messaggio: opec228.20181011171020.22547.05.1.09@pec-emai= 26 | l.com 27 | 28 | ------------=_1539270645-22547-8 29 | Content-Type: application/xml; name="cert.xml" 30 | Content-Disposition: inline; filename="cert.xml" 31 | Content-Transfer-Encoding: base64 32 | 33 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 34 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 35 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 36 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 37 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 38 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 39 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 40 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 41 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 42 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 43 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 44 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 45 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 46 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 47 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 48 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 49 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 50 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 51 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 52 | bGV0YSIgLz4KICAgIDwvZGF0aT4KPC9wb3N0YWNlcnQ+Cg== 53 | 54 | ------------=_1539270645-22547-8 55 | Content-Type: message/rfc822; name="postacert.eml" 56 | Content-Disposition: inline; filename="postacert.eml" 57 | Content-Transfer-Encoding: 7bit 58 | 59 | From: services@pec.services.com 60 | To: certified@registerpec.it 61 | Subject: Contratti e Documentazione 62 | MIME-Version: 1.0 63 | Content-Type: multipart/mixed; 64 | boundary="----=_Part_859294_104690269.1539270139866" 65 | X-Priority: 3 66 | Importance: Medium 67 | X-TipoRicevuta: completa 68 | X-Mailer: Open-Xchange Mailer v7.8.3-Rev44 69 | X-Originating-Client: open-xchange-appsuite 70 | Message-ID: 71 | X-Riferimento-Message-ID: <983049506.859298.1539270139890@www.pec.net> 72 | 73 | ------=_Part_859294_104690269.1539270139866 74 | Content-Type: multipart/alternative; 75 | boundary="----=_Part_859295_740244190.1539270139866" 76 | 77 | ------=_Part_859295_740244190.1539270139866 78 | MIME-Version: 1.0 79 | Content-Type: text/plain; charset=UTF-8 80 | Content-Transfer-Encoding: 7bit 81 | 82 | Cordiali Saluti, 83 | 84 | http://www.services.com/ 85 | 86 | 87 | ------=_Part_859295_740244190.1539270139866 88 | Content-Type: multipart/related; 89 | boundary="----=_Part_859296_911334259.1539270139866" 90 | 91 | ------=_Part_859296_911334259.1539270139866 92 | MIME-Version: 1.0 93 | Content-Type: text/html; charset=UTF-8 94 | Content-Transfer-Encoding: 7bit 95 | 96 | 97 | 98 | 99 |

Cordiali Saluti,




Services


Services


 

100 | ------=_Part_859296_911334259.1539270139866-- 101 | 102 | ------=_Part_859295_740244190.1539270139866-- 103 | 104 | ------=_Part_859294_104690269.1539270139866 105 | Content-Type: text/plain; 106 | charset=US-ASCII; 107 | delsp=yes; 108 | format=flowed 109 | Content-Transfer-Encoding: 7bit 110 | 111 | This is the first part. 112 | ------=_Part_859294_104690269.1539270139866 113 | Content-Type: text/plain; 114 | charset=US-ASCII; 115 | delsp=yes; 116 | format=flowed 117 | Content-Transfer-Encoding: 7bit 118 | 119 | This is the second part. 120 | ------=_Part_859294_104690269.1539270139866-- 121 | 122 | ------------=_1539270645-22547-8-- 123 | ------------=_1539270648-22547-9 124 | Content-Type: application/pkcs7-signature; name="smime.p7s" 125 | Content-Transfer-Encoding: base64 126 | Content-Disposition: attachment; filename="smime.p7s" 127 | 128 | MIIamQYJKoZIhvcNAQcCoIIaijCCGoYCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0BBwGg 129 | ghdDMIIFfzCCBGegAwIBAgIIaF/rZg51FdswDQYJKoZIhvcNAQELBQAwgZAxCzAJBgNVBAYTAklU 130 | MQ0wCwYDVQQHDARSb21hMSYwJAYDVQQKDB1BZ2VuemlhIHBlciBsJ0l0YWxpYSBEaWdpdGFsZTE3 131 | MDUGA1UECwwuQXJlYSBTb2x1emlvbmkgcGVyIGxhIFB1YmJsaWNhIEFtbWluaXN0cmF6aW9uZTER 132 | MA8GA1UEAwwIQWdJRCBDQTEwHhcNMTcxMTI4MTUzOTAxWhcNMjAxMTI3MTUzOTAxWjBoMQswCQYD 133 | VQQGEwJJVDEQMA4GA1UECAwHRmlyZW56ZTEQMA4GA1UEBwwHRmlyZW56ZTEbMBkGA1UECgwSUmVn 134 | aXN0ZXIuaXQgUy5wLkEuMRgwFgYDVQQDDA9SZWdpc3Rlci5pdCBQRUMwggEiMA0GCSqGSIb3DQEB 135 | AQUAA4IBDwAwggEKAoIBAQCLNHmQTbeVO2A+m1gX+7BlzzeC+b39T8m/b3DON7KPxLXQUt9UlW3c 136 | 8AvPo+Vee2AQbgmu6eMYTUmx9UVjXwYFNm3bkGh/Q5TkJGVtXnCTnOzCWrzHsTErmcIVRIDX/SyH 137 | qOiUsVwZW7eCunNBK97mQPoWiX6vrzGFs1RFnueaT8/xlYP6mm5OSOofFUGhiPyrydBDzJ+xOknQ 138 | 14ZPiA5msioRXgHrD4UxxvAqs8UCodKDjrh72FQYbuVutPluNTzr0LpK/pbK1y2zSzYkZVaZOAoI 139 | ZBCcQM/AI0CwK5990PWYNaZBIB07OsD15WyFP8QkRAHZ7qAjOV8pwvZY9dT5AgMBAAGjggICMIIB 140 | /jA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9jYTEuYWdpZC5nb3YuaXQvT0NT 141 | UDAdBgNVHQ4EFgQUGULFuQ5q3G+B4EQRSazlvJYXlOIwHwYDVR0jBBgwFoAUpf2FBQ7D8dZlSiBs 142 | 4ttNYJMriqAwTwYDVR0gBEgwRjBEBgYrTBADAQEwOjA4BggrBgEFBQcCARYsaHR0cDovL3d3dy5h 143 | Z2lkLmdvdi5pdC9jZXJ0aWZpY2F0aS1maXJtYS1wZWMwgeAGA1UdHwSB2DCB1TCBsKCBraCBqoaB 144 | p2xkYXA6Ly9jYTEuYWdpZC5nb3YuaXQvY249QWdJRCUyMENBMSxvdT1BcmVhJTIwU29sdXppb25p 145 | JTIwcGVyJTIwbGElMjBQdWJibGljYSUyMEFtbWluaXN0cmF6aW9uZSxvPUFnZW56aWElMjBwZXIl 146 | MjBsJTI3SXRhbGlhJTIwRGlnaXRhbGUsQz1JVD9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0MCCg 147 | HqAchhpodHRwOi8vY2ExLmFnaWQuZ292Lml0L0NSTDAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAww 148 | CgYIKwYBBQUHAwQwKgYDVR0RBCMwIYEfcG9zdGEtY2VydGlmaWNhdGFAcGVjLWVtYWlsLmNvbTAN 149 | BgkqhkiG9w0BAQsFAAOCAQEAJfKcwg4weaUMuXbPgZwfXzFWdL5QF1XJLn9AbaPly18SaTyai81S 150 | ZU5H35vIrBk9ZWq3MB44xcsaRorLjdLCD1bcobOL//U7zOxFU6VsGwiVwsrJctbt9v045QwBcRkS 151 | v1V77QbkshG5SZikMTA0gRk+xWEHIGLWFIQAXvFUaJXEe3GoCKPGeXwjJ1RG/YMHNQJgeL8VWqN2 152 | vKb/iio1ghe6wRc4uTRNlFEgMhUmvXJMSYbnr9BQLObkHwdNI+9OVVYx7t63icsUAnxO19Jdn/UI 153 | +yspSU/b6Jd+MaCxjTOJLzwAgDqXzWeF4+NMAPUYGt0Gi1fCXu13sN0zGAre7DCCEbwwgg+koAMC 154 | AQICCEmBpuNTz9qlMA0GCSqGcIb3DQEBCwUAMGsxCzAJBgNVBAYTAklUMQ4wDAYDVQQHDAVNaWxh 155 | bjEjMCEGA1UECgwaQWN0YWxpcyBTLnAuQS4vMDMzNTg1MjA5NjcxJzAlBgNVBAMMHkFjdGFsaXMg 156 | QXV0aGVudGljYXRpb24gUm9vdCBDQTAeFw0xNzEwMTIxMzI5NTNaFw0yNzEwMTIxMzI5NTNaMIGQ 157 | MQswCQYDVQQGEwJJVDENMAsGA1UEBwwEUm9tYTEmMCQGA1UECgwdQWdlbnppYSBwZXIgbCdJdGFs 158 | aWEgRGlnaXRhbGUxNzA1BgNVBAsMLkFyZWEgU29sdXppb25pIHBlciBsYSBQdWJibGljYSBBbW1p 159 | bmlzdHJhemlvbmUxETAPBgNVBAMMCEFnSUQgQ0ExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 160 | CgKCAQEA1TafReuLzB6kjYBqvMlUbkT3+RCleG4ttfexCA3RGNN+G88H1/BdGt+9qvm6mxYa/A+5 161 | QHZncDaOx4NrWFu8d55+SR+MpP9SRSJ2pkQqmtvXvq3/Mmg1UjuWBEKOPP3QHJWNQcgW7BBQc5Ux 162 | t8fJ4FalNCvNizGeh9EpF6ndkRIuWEpqWfj/z+tIgLGcPwql5NDjVCfxVimPRFXQ1TT6T1WBH0L8 163 | rjNfXPgCicx6ZxE//wHVcXKOTYtqpRce3wsHDpZv45g76f2tcpPykPKv3/N36RInHEjaC+ijJ7te 164 | 3fEkao4OSlmu7dDcZvlzxPZRnaacpnA5yI3in/m1w5oLVwIDAQABo4INPDCCDTgwEgYDVR0TAQH/ 165 | BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwTgYDVR0gBEcwRTBDBgUrTBADATA6MDgGCCsGAQUF 166 | BwIBFixodHRwOi8vd3d3LmFnaWQuZ292Lml0L2NlcnLpZmljYXRpLWZpcm1hLXBlYzAdBgNVHQ4E 167 | FgQUpf2FBQ7D8dZlSiBs4ttNYJMriqAwgeMGA1UdHwSB2zCB2DCBlqCBk6CBkIaBjWxkYXA6Ly9s 168 | ZGFwMDcuYWN0YWxpcy5pdC9jbiUzZEFjdGFsaXMlMjBBdXRoZW50aWNhdGlvbiUyMFJvb3QlMjBD 169 | QSxvJTNkQWN0YWxpcyUyMFMucC5BLiUyZjAzMzU4NTIwOTY3LGMlM2RJVD9jZXJ0aWZpY2F0ZVJl 170 | dm9jYXRpb25MaXN0O2JpbmFyeTA9oDugOYY3aHR0cDovL2NybDA3LmFjdGFsaXMuaXQvUmVwb3Np 171 | dG9yeS9BVVRILVJPT1QvZ2V0TGFzdENSTDBBBggrBgEFBQcBAQQ1MDMwMQYIKwYBBQUHMAGGJWh0 172 | dHA6Ly9vY3NwMDcuYWN0YWxpcy5pdC9WQS9BVVRILVJPT1QwHwYDVR0jBBgwFoAUUtiIOsifeGbt 173 | ifN7OHCUyQICNtAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMEMIILOAYDVR0eBIILLzCC 174 | Cyugggr1MA2BC2FnaWQuZ292Lml0MBmBF2NlcnQuaW5mb3JtYXRpY2EuYWNpLml0MBGBD2NlcnQu 175 | aW50ZXJuby5pdDAMgQplbWFyY2hlLml0MBCBDmZhc3R3ZWItcGVjLml0MBWBE2dlc3RvcmVwZWMu 176 | dW5pbmEuaXQwDIEKa21haWxlci5pdDAOgQxsZWdhbG1haWwuaXQwEIEOcGNlcnQuc29nZWkuaXQw 177 | EIEOcGVjLmFjdGFsaXMuaXQwEIEOcGVjLmFuY2l0ZWwuaXQwDoEMcGVjLmFydWJhLml0MBaBFHBl 178 | Yy5iYXNpbGljYXRhbmV0Lml0MA+BDXBlYy1lbWFpbC5jb20wD4ENcGVjLm51bWVyYS5pdDASgRBw 179 | ZWMucG9zdGVjZXJ0Lml0MBaBFHBlYy5wb3N0ZWl0YWxpYW5lLml0MBWBE3BlYy5ydXBhci5wdWds 180 | aWEuaXQwFoEUcG9zdGFjZXJ0LmNlZGFjcmkuaXQwH4EdcG9zdGFjZXJ0aWZpY2F0YS5ub3Rhcmlh 181 | dG8uaXQwEoEQcG9zdGFjZXJ0Lml0Lm5ldDAOgQxwb3N0ZWNlcnQuaXQwFYETc2ljdXJlenphcG9z 182 | dGFsZS5pdDAQgQ50ZWxlY29tcG9zdC5pdDAXgRV0cnVzdGVkbWFpbC5pbnRlc2EuaXQwDIEKdHd0 183 | Y2VydC5pdDARgQ97dWNjaGV0dGlwZWMuaXQwEoIQYWdpZC1jYTEtdGVzdC5pdDANggthZ2lkLmdv 184 | di5pdDAfgh1hdmFuemFtZW50b2RpZ2l0YWxlLml0YWxpYS5pdDAMggpjZXJ0LXBhLml0MAqCCGNu 185 | aXBhLml0MBuCGWZhc2NpY29sb3Nhbml0YXJpby5nb3YuaXQwEYIPaW5kaWNlcGEuZ292Lml0MA+C 186 | DXBhZ29wYS5nb3YuaXQwGYIXcG9zdGFjZXJ0aWZpY2F0YS5nb3YuaXQwDYILcm5kdC5nb3YuaXQw 187 | D4INc3Bjb29wLmdvdi5pdDANggtzcGlkLmdvdi5pdDAOggxpY3NwYy5nb3YuaXQwUKROMEwxCzAJ 188 | BgNVBAYTAklUMQ0wCwYDVQQIDARSb21hMQ0wCwYDVQQHDARSb21hMR8wHQYDVQQKDBZBQ0kgSW5m 189 | b3JtYXRpY2EgUy5wLkEuMFmkVzBVMQswCQYDVQQGEwJJVDEQMA4GA1UECAwHQmVyZ2FtbzEZMBcG 190 | A1UEBwwQUG9udGUgU2FuIFBpZXRybzEZMBcGA1UECgwQQXJ1YmEgUEVDIFMucC5BLjBXpFUwUzEL 191 | MAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRlIFNhbiBQaWV0cm8x 192 | FzAVBgNVBAoMDkFjdGFsaXMgUy5wLkEuMFekVTBTMQswCQYDVQQGEwJJVDENMAsGA1UECAwEUm9t 193 | YTENMAsGA1UEBwwEUm9tYTEmMCQGA1UECgwdQWdlbnppYSBwZXIgbCdJdGFsaWEgRGlnaXRhbGUw 194 | SKRGMEQxCzAJBgNVBAYTAklUMQ0wCwYDVQQIDARSb21hMQ0wCwYDVQQHDARSb21hMRcwFQYDVQQK 195 | DA5BbmNpdGVsIFMucC5BLjBPpE0wSzELMAkGA1UEBhMCSVQxDjAMBgNVBAgMBVBhcm1hMRMwEQYD 196 | VQQHDApDb2xsZWNjaGlvMRcwFQYDVQQKDA5DZWRhY3JpIFMucC5BLjBbpFkwVzELMAkGA1UEBhMC 197 | SVQxDTALBgNVBAgMBFJvbWExDTALBgNVBAcMBFJvbWExKjAoBgNVBAoMIUNvbnNpZ2xpbyBOYXpp 198 | b25hbGUgZGVsIE5vdGFyaWF0bzBMpEowSDELMAkGA1UEBhMCSVQxDzANBgNVBAgMBk1pbGFubzEP 199 | MA0GA1UEBwwGTWlsYW5vMRcwFQYDVQQKDA5GYXN0d2ViIFMucC5BLjBKpEgwRjELMAkGA1UEBhMC 200 | SVQxDzANBgNVBAgMBk1pbGFubzEPMA0GA1UEBwwGQXNzYWdvMRUwEwYDVQQKDAxJVG5ldCBTLnIu 201 | bC4wT6RNMEsxCzAJBgNVBAYTAklUMQ8wDQYDVQQIDAZUb3Jpbm8xDzANBgNVBAcMBlRvcmlubzEa 202 | MBgGA1UECgwRSW4uVGUuUy5BLiBTLnAuQS4wSaRHMEUxCzAJBgNVBAYTAklUMQ0wCwYDVQQIDARS 203 | b21hMQ0wCwYDVQQHDARSb21hMRgwFgYDVQQKDA9JbmZvQ2VydCBTLnAuQS4wUqRQME4xCzAJBgNV 204 | BAYTAklUMQ0wCwYDVQQIDARCYXJpMRIwEAYDVQQHDAlWYWxlbnphbm8xHDAaBgNVBAoME0lubm92 205 | YVB1Z2xpYSBTLnAuQS4wVKRSMFAxCzAJBgNVBAYTAklUMQ8wDQYDVQQIDAZNaWxhbm8xDzANBgNV 206 | BAcMBk1pbGFubzEfMB0GA1UECgwWS1BOUXdlc3QgSXRhbGlhIFMucC5BLjBRpE8wTTELMAkGA1UE 207 | BhMCSVQxDzANBgNVBAgMBkFuY29uYTETMBEGA1UEBwwKU2VubWdhbGxpYTEYMBYGA1UECgwPTmFt 208 | aXJpYWwgUy5wLkEuMGOkYTBfMQswCQYDVQQGEwJJVDEQMA4GA1UECAwHU2Fzc2FyaTEQMA4GA1UE 209 | BwwHU2Fzc2FyaTEsMCoGA1UECgwjTnVtZXJhIFNpc3RlbWkgZSBJbmZvcm1hdGljYSBTLnAuQS4w 210 | T6RNMEsxCzAJBgNVBAYTAklUMQ0wCwYDVQQIDARSb21hMQ0wCwYDVQQHDARSb21hMR4wHAYDVQQK 211 | DBVQb3N0ZSBJdGFsaWFuZSBTLnAuQS4wUqRQME4xCzAJBgNVBAYTAklUMRAwDgYDVQQIDAdQb3Rl 212 | bnphMRAwDgYDVQQHDAdQb3RlbnphMRswGQYDVQQKDBJSZWdpb25lIEJhc2lsaWNhdGEwTKRKMEgx 213 | CzAJBgNVBAYTAklUMQ8wDQYDVQQIDAZBbmNvbmExDzANBgNVBAcMBkFuY29uYTEXMBUGA1UECgwO 214 | UmVnaW9uZSBNYXJjaGUwUqRQME4xCzAJBgNVBAYTAklUMRAwDgYDVQQIDAdGaXJlbnplMRAwDgYD 215 | VQQHDAdGaXJlbnplMRswGQYDVQQKDBJSZWdpc3Rlci5pdCBTLnAuQS4wRqREMEIxCzAJBgNVBAYT 216 | AklUMQ0wCwYDVQQIDARSb21hMQ0wCwYDVQQHDARSb21hMRUwEwYDVQQKDAxTb2dlaSBTLnAuQS4w 217 | SKRGMEQxCzAJBgNVBAYTAklUMQ8wDQYDVQQIDAZNaWxhbm8xDzANBgNVBAcMBk1pbGFubzETMBEG 218 | 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 248 | ------------=_1539270648-22547-9-- 249 | -------------------------------------------------------------------------------- /src/Message/Header.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE.md. 10 | */ 11 | 12 | namespace Phemail\Message; 13 | 14 | /** 15 | * Class Header 16 | */ 17 | class Header implements HeaderInterface 18 | { 19 | /** 20 | * @var string 21 | */ 22 | protected $value; 23 | 24 | /** 25 | * @var string[] 26 | */ 27 | protected $attributes = []; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function getValue() 33 | { 34 | return $this->value; 35 | } 36 | 37 | /** 38 | * @param string $value 39 | * @return static 40 | */ 41 | public function withValue($value) 42 | { 43 | $clone = clone $this; 44 | $clone->value = $value; 45 | 46 | return $clone; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getAttributes() 53 | { 54 | return $this->attributes; 55 | } 56 | 57 | /** 58 | * @param string $name 59 | * @param string $value 60 | * @return static 61 | */ 62 | public function withAttribute($name, $value) 63 | { 64 | $clone = clone $this; 65 | $clone->attributes[$name] = $value; 66 | 67 | return $clone; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function getAttribute($name) 74 | { 75 | return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : null; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Message/HeaderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE.md. 10 | */ 11 | 12 | namespace Phemail\Message; 13 | 14 | /** 15 | * Interface HeaderInterface 16 | */ 17 | interface HeaderInterface 18 | { 19 | /** 20 | * @return string 21 | */ 22 | public function getValue(); 23 | 24 | /** 25 | * @return string[] 26 | */ 27 | public function getAttributes(); 28 | 29 | /** 30 | * @param string $name 31 | * @return string 32 | */ 33 | public function getAttribute($name); 34 | } 35 | -------------------------------------------------------------------------------- /src/Message/MessagePart.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE.md. 10 | */ 11 | 12 | namespace Phemail\Message; 13 | 14 | /** 15 | * Class MessagePart 16 | */ 17 | class MessagePart implements MessagePartInterface 18 | { 19 | /** 20 | * @var HeaderInterface[] 21 | */ 22 | protected $headers = []; 23 | 24 | /** 25 | * @var string 26 | */ 27 | protected $contents; 28 | 29 | /** 30 | * @var MessagePartInterface[] 31 | */ 32 | protected $attachments = []; 33 | 34 | /** 35 | * @var MessagePartInterface[] 36 | */ 37 | protected $parts = []; 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function getHeaders() 43 | { 44 | return $this->headers; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getHeader($name) 51 | { 52 | return array_key_exists($name, $this->headers) ? $this->headers[$name] : null; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getHeaderAttribute($header, $attr, $default = null) 59 | { 60 | $header = $this->getHeader($header); 61 | if ($header && ($attribute = $header->getAttribute($attr))) { 62 | return $attribute; 63 | } 64 | 65 | return $default; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function getHeaderValue($name, $default = null) 72 | { 73 | $header = $this->getHeader($name); 74 | 75 | return $header ? $header->getValue() : $default; 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function withHeader($name, HeaderInterface $header) 82 | { 83 | $clone = clone $this; 84 | $clone->headers[$name] = $header; 85 | 86 | return $clone; 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function getContentType() 93 | { 94 | return $this->getHeaderValue('content-type', 'text/plain'); 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function isMultiPart() 101 | { 102 | return stripos($this->getContentType(), 'multipart/') === 0; 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | public function isMessage() 109 | { 110 | return stripos($this->getContentType(), 'message/') === 0; 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | public function isText() 117 | { 118 | return stripos($this->getContentType(), 'text/') === 0; 119 | } 120 | 121 | /** 122 | * {@inheritdoc} 123 | */ 124 | public function getContents() 125 | { 126 | return $this->contents; 127 | } 128 | 129 | /** 130 | * @param string $contents 131 | * @return static 132 | */ 133 | public function withContents($contents) 134 | { 135 | $clone = clone $this; 136 | $clone->contents = $contents; 137 | 138 | return $clone; 139 | } 140 | 141 | /** 142 | * {@inheritdoc} 143 | */ 144 | public function getParts($recursive = false) 145 | { 146 | $ret = $this->parts; 147 | if ($recursive) { 148 | foreach ($this->parts as $part) { 149 | $ret = array_merge($ret, $part->getParts(true)); 150 | } 151 | } 152 | 153 | return $ret; 154 | } 155 | 156 | /** 157 | * @return static 158 | */ 159 | public function withPart(MessagePartInterface $part) 160 | { 161 | $clone = clone $this; 162 | if ($part->getHeaderValue('content-disposition') === 'attachment') { 163 | $clone->attachments[] = $part; 164 | } else { 165 | $clone->parts[] = $part; 166 | } 167 | 168 | return $clone; 169 | } 170 | 171 | /** 172 | * {@inheritdoc} 173 | */ 174 | public function getAttachments($recursive = false) 175 | { 176 | $ret = $this->attachments; 177 | if ($recursive) { 178 | foreach ($this->parts as $part) { 179 | $ret = array_merge($ret, $part->getAttachments(true)); 180 | } 181 | } 182 | 183 | return $ret; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Message/MessagePartInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE.md. 10 | */ 11 | 12 | namespace Phemail\Message; 13 | 14 | /** 15 | * Interface MessagePartInterface 16 | */ 17 | interface MessagePartInterface 18 | { 19 | /** 20 | * @return HeaderInterface[] 21 | */ 22 | public function getHeaders(); 23 | 24 | /** 25 | * @param string $name 26 | * @return HeaderInterface 27 | */ 28 | public function getHeader($name); 29 | 30 | /** 31 | * @param string $header 32 | * @param string $attr 33 | * @param string $default 34 | * @return string 35 | */ 36 | public function getHeaderAttribute($header, $attr, $default = null); 37 | 38 | /** 39 | * @param string $name 40 | * @param string $default 41 | * @return string 42 | */ 43 | public function getHeaderValue($name, $default = null); 44 | 45 | /** 46 | * @return bool 47 | */ 48 | public function isMultiPart(); 49 | 50 | /** 51 | * @return bool 52 | */ 53 | public function isMessage(); 54 | 55 | /** 56 | * @return bool 57 | */ 58 | public function isText(); 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getContentType(); 64 | 65 | /** 66 | * @return string 67 | */ 68 | public function getContents(); 69 | 70 | /** 71 | * @return MessagePartInterface[] 72 | */ 73 | public function getAttachments($recursive = false); 74 | 75 | /** 76 | * @return MessagePartInterface[] 77 | */ 78 | public function getParts($recursive = false); 79 | } 80 | -------------------------------------------------------------------------------- /src/MessageParser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE.md. 10 | */ 11 | 12 | namespace Phemail; 13 | 14 | use Phemail\Message\Header; 15 | use Phemail\Message\MessagePart; 16 | 17 | /** 18 | * Class MessageParser 19 | */ 20 | class MessageParser implements MessageParserInterface 21 | { 22 | const REGEX_HEADER_LINE = '~^(?![\s]+)(?[^:]+):(\s+(?(?[^;]+).*))?$~'; 23 | 24 | const REGEX_HEADER_LINE_EXTENDED = '~^\s+(?.*)$~'; 25 | 26 | const REGEX_ATTRIBUTE = '~[;\s]+(?[^=]+)=(?:["])?(?[^;"]+)(?:["])?~'; 27 | 28 | /* 29 | * {@inheritdoc} 30 | */ 31 | public function parse($payload, $withSubMesssage = true) 32 | { 33 | if (is_string($payload)) { 34 | $iterator = new \ArrayIterator(file($payload, FILE_IGNORE_NEW_LINES)); 35 | } elseif (is_array($payload)) { 36 | $iterator = new \ArrayIterator($payload); 37 | } elseif ($payload instanceof \Iterator) { 38 | $iterator = $payload; 39 | } else { 40 | throw new \InvalidArgumentException('$payload must be either string, array or an instance of \\Iterator'); 41 | } 42 | $message = $this->parseHeaders($iterator, $message = new MessagePart()); 43 | $message = $this->parseMessage($iterator, $message, null, $withSubMesssage); 44 | 45 | return $message; 46 | } 47 | 48 | /** 49 | * @return MessagePart 50 | */ 51 | protected function parseHeaders(\Iterator $lines, MessagePart $part) 52 | { 53 | while ($lines->valid()) { 54 | $line = $lines->current(); 55 | if (empty($line)) { 56 | break; 57 | } 58 | if (preg_match(self::REGEX_HEADER_LINE, $line, $matches)) { 59 | while ($lines->valid()) { 60 | $lines->next(); 61 | $line = $lines->current(); 62 | if (preg_match(self::REGEX_HEADER_LINE_EXTENDED, $line, $matches2)) { 63 | $matches['content'] .= (array_key_exists('content', $matches) ? ' ' : '').trim($matches2['content']); 64 | 65 | continue; 66 | } 67 | break; 68 | } 69 | $matches['name'] = strtolower($matches['name']); 70 | $header = new Header(); 71 | 72 | switch ($matches['name']) { 73 | case 'content-disposition': 74 | case 'content-type': 75 | $header = $header->withValue($matches['value']); 76 | if (preg_match_all(self::REGEX_ATTRIBUTE, $matches['content'], $attributes)) { 77 | foreach ($attributes['name'] as $i => $attribute) { 78 | $header = $header->withAttribute($attribute, $attributes['value'][$i]); 79 | } 80 | } 81 | break; 82 | default: 83 | $header = $header->withValue($matches['content']); 84 | break; 85 | } 86 | $part = $part->withHeader($matches['name'], $header); 87 | } else { 88 | $lines->next(); 89 | } 90 | } 91 | 92 | return $part; 93 | } 94 | 95 | /** 96 | * @param bool $withSubMesssage 97 | * @param bool $parseSubMessage 98 | * @return MessagePart 99 | */ 100 | protected function parseMessage(\Iterator $lines, MessagePart $part, $boundary = null, $withSubMesssage = true, $parseSubMessage = true) 101 | { 102 | if ($part->isMultiPart()) { 103 | $boundary = $part->getHeaderAttribute('content-type', 'boundary'); 104 | while ($lines->valid()) { 105 | $line = trim($lines->current()); 106 | $lines->next(); 107 | if ($line === "--$boundary") { 108 | $sub = $this->parseHeaders($lines, $sub = new MessagePart()); 109 | $sub = $this->parseMessage($lines, $sub, $boundary, $withSubMesssage, $withSubMesssage); 110 | $part = $part->withPart($sub); 111 | } elseif ($line === "--$boundary--") { 112 | break; 113 | } 114 | } 115 | 116 | return $part; 117 | } elseif ($part->isMessage() && $parseSubMessage) { 118 | $lines->next(); 119 | $sub = $this->parseHeaders($lines, $sub = new MessagePart()); 120 | $sub = $this->parseMessage($lines, $sub, $boundary, $withSubMesssage, $withSubMesssage); 121 | 122 | return $part->withPart($sub); 123 | } else { 124 | if ($part->isMessage()) { 125 | $lines->next(); 126 | } 127 | 128 | return $part->withContents($this->parseContent($lines, $boundary)); 129 | } 130 | } 131 | 132 | /** 133 | * @return string 134 | */ 135 | protected function parseContent(\Iterator $lines, $boundary) 136 | { 137 | $contents = []; 138 | while ($lines->valid()) { 139 | $line = $lines->current(); 140 | $trimmed = trim($line); 141 | if (is_null($boundary) || ($trimmed !== "--$boundary" && $trimmed !== "--$boundary--")) { 142 | $contents[] = $line; 143 | } else { 144 | break; 145 | } 146 | $lines->next(); 147 | } 148 | 149 | return implode(PHP_EOL, $contents); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/MessageParserInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE.md. 10 | */ 11 | 12 | namespace Phemail; 13 | 14 | use Phemail\Message\MessagePartInterface; 15 | 16 | /** 17 | * Interface MessageParserInterface 18 | */ 19 | interface MessageParserInterface 20 | { 21 | /** 22 | * @param string|array|\Iterator $payload 23 | * @return MessagePartInterface 24 | */ 25 | public function parse($payload); 26 | } 27 | -------------------------------------------------------------------------------- /tests/MessageParserTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE.md. 10 | */ 11 | 12 | namespace Phemail\tests; 13 | 14 | //include "./vendor/autoload.php"; 15 | 16 | use Phemail\MessageParser; 17 | use Phemail\MessageParserInterface; 18 | 19 | /** 20 | * Class MessageParserTest 21 | */ 22 | class MessageParserTest extends \PHPUnit_Framework_TestCase 23 | { 24 | /** 25 | * @var MessageParserInterface 26 | */ 27 | protected $parser; 28 | 29 | public function setUp() 30 | { 31 | $this->parser = new MessageParser(); 32 | } 33 | 34 | public function testPlainEmail() 35 | { 36 | $message = $this->parser->parse(__DIR__.'/../sample/plain.eml'); 37 | $this->assertTrue($message->isText()); 38 | $this->assertFalse($message->isMultiPart()); 39 | $this->assertCount(8, $message->getHeaders()); 40 | $this->assertEquals('1.0', $message->getHeaderValue('mime-version')); 41 | $this->assertEquals('Testing simple email', $message->getHeaderValue('subject')); 42 | $this->assertEquals('text/plain', $message->getHeaderValue('content-type')); 43 | $this->assertCount(2, $message->getHeader('content-type')->getAttributes()); 44 | $this->assertEquals('US-ASCII', $message->getHeaderAttribute('content-type', 'charset')); 45 | $this->assertEquals('flowed', $message->getHeaderAttribute('content-type', 'format')); 46 | $this->assertEquals('7bit', $message->getHeaderValue('content-transfer-encoding')); 47 | $this->assertNotEmpty($contents = $message->getContents()); 48 | $this->assertInternalType('string', $contents); 49 | } 50 | 51 | public function testMultiPartEmail() 52 | { 53 | $message = $this->parser->parse(__DIR__.'/../sample/multipart.eml'); 54 | $this->assertTrue($message->isMultiPart()); 55 | $this->assertFalse($message->isText()); 56 | $this->assertCount(7, $message->getHeaders()); 57 | $this->assertEquals('1.0', $message->getHeaderValue('mime-version')); 58 | $this->assertEquals('Testing multipart email', $message->getHeaderValue('subject')); 59 | $this->assertEquals('multipart/mixed', $message->getHeaderValue('content-type')); 60 | $this->assertCount(1, $message->getHeader('content-type')->getAttributes()); 61 | $this->assertEquals('652b8c4dcb00cdcdda1e16af36781caf', $message->getHeaderAttribute('content-type', 'boundary')); 62 | $this->assertEmpty($contents = $message->getContents()); 63 | $this->assertCount(1, $attachments = $message->getAttachments()); 64 | $this->assertFalse($attachments[0]->isMultiPart()); 65 | $this->assertEquals('text/x-ruby-script', $attachments[0]->getHeaderValue('content-type')); 66 | $this->assertCount(1, $attachments[0]->getHeader('content-type')->getAttributes()); 67 | $this->assertEquals('hello.rb', $attachments[0]->getHeaderAttribute('content-type', 'name')); 68 | $this->assertCount(1, $attachments[0]->getHeader('content-disposition')->getAttributes()); 69 | $this->assertEquals('api.rb', $attachments[0]->getHeaderAttribute('content-disposition', 'filename')); 70 | $this->assertCount(2, $parts = $message->getParts()); 71 | $this->assertTrue($parts[0]->isText()); 72 | $this->assertFalse($parts[0]->isMultiPart()); 73 | $this->assertEquals('text/plain', $parts[0]->getHeaderValue('content-type')); 74 | $this->assertCount(3, $parts[0]->getHeader('content-type')->getAttributes()); 75 | $this->assertEquals('US-ASCII', $parts[0]->getHeaderAttribute('content-type', 'charset')); 76 | $this->assertEquals('yes', $parts[0]->getHeaderAttribute('content-type', 'delsp')); 77 | $this->assertEquals('flowed', $parts[0]->getHeaderAttribute('content-type', 'format')); 78 | $this->assertEquals('7bit', $parts[0]->getHeaderValue('content-transfer-encoding')); 79 | $this->assertNotEmpty($contents = $parts[0]->getContents()); 80 | $this->assertInternalType('string', $contents); 81 | $this->assertTrue($parts[1]->isText()); 82 | $this->assertFalse($parts[1]->isMultiPart()); 83 | $this->assertEquals('text/html', $parts[1]->getHeaderValue('content-type')); 84 | $this->assertCount(2, $parts[1]->getHeader('content-type')->getAttributes()); 85 | $this->assertEquals('ISO-8859-1', $parts[1]->getHeaderAttribute('content-type', 'charset')); 86 | $this->assertEquals('flowed', $parts[1]->getHeaderAttribute('content-type', 'format')); 87 | $this->assertEquals('quoted-printable', $parts[1]->getHeaderValue('content-transfer-encoding')); 88 | $this->assertNotEmpty($contents = $parts[1]->getContents()); 89 | $this->assertInternalType('string', $contents); 90 | $this->assertCount(1, $message->getAttachments(true)); 91 | } 92 | 93 | public function testRfc822Email() 94 | { 95 | $message = $this->parser->parse(__DIR__.'/../sample/rfc822.eml'); 96 | $this->assertTrue($message->isMultiPart()); 97 | $this->assertFalse($message->isText()); 98 | $this->assertFalse($message->isText()); 99 | $this->assertCount(3, $message->getHeader('content-type')->getAttributes()); 100 | $this->assertEmpty($contents = $message->getContents()); 101 | $this->assertCount(1, $attachments = $message->getAttachments()); 102 | $this->assertCount(1, $parts = $message->getParts()); 103 | $this->assertTrue($parts[0]->isMultipart()); 104 | $this->assertCount(3, $parts = $parts[0]->getParts()); 105 | $this->assertTrue($parts[0]->isText()); 106 | $this->assertEquals('application/xml', $parts[1]->getContentType()); 107 | $this->assertTrue($parts[2]->isMessage()); 108 | $this->assertEmpty($parts[2]->getContents()); 109 | $this->assertCount(1, $parts = $parts[2]->getParts()); 110 | $this->assertTrue($parts[0]->isMultipart()); 111 | $this->assertCount(3, $parts = $parts[0]->getParts()); 112 | $this->assertTrue($parts[0]->isMultipart()); 113 | $this->assertTrue($parts[1]->isText()); 114 | $this->assertTrue($parts[2]->isText()); 115 | $this->assertCount(1, $message->getAttachments(true)); 116 | $this->assertCount(11, $message->getParts(true)); 117 | } 118 | } 119 | --------------------------------------------------------------------------------