├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Address.php ├── Address └── AddressInterface.php ├── AddressList.php ├── ConfigProvider.php ├── Exception ├── BadMethodCallException.php ├── DomainException.php ├── ExceptionInterface.php ├── InvalidArgumentException.php ├── OutOfBoundsException.php └── RuntimeException.php ├── Header ├── AbstractAddressList.php ├── Bcc.php ├── Cc.php ├── ContentTransferEncoding.php ├── ContentType.php ├── Date.php ├── Exception │ ├── BadMethodCallException.php │ ├── ExceptionInterface.php │ ├── InvalidArgumentException.php │ └── RuntimeException.php ├── From.php ├── GenericHeader.php ├── GenericMultiHeader.php ├── HeaderInterface.php ├── HeaderLoader.php ├── HeaderName.php ├── HeaderValue.php ├── HeaderWrap.php ├── IdentificationField.php ├── InReplyTo.php ├── ListParser.php ├── MessageId.php ├── MimeVersion.php ├── MultipleHeadersInterface.php ├── Received.php ├── References.php ├── ReplyTo.php ├── Sender.php ├── StructuredInterface.php ├── Subject.php ├── To.php └── UnstructuredInterface.php ├── Headers.php ├── Message.php ├── MessageFactory.php ├── Module.php ├── Protocol ├── AbstractProtocol.php ├── Exception │ ├── ExceptionInterface.php │ ├── InvalidArgumentException.php │ └── RuntimeException.php ├── Imap.php ├── Pop3.php ├── ProtocolTrait.php ├── Smtp.php ├── Smtp │ └── Auth │ │ ├── Crammd5.php │ │ ├── Login.php │ │ └── Plain.php ├── SmtpPluginManager.php └── SmtpPluginManagerFactory.php ├── Storage.php ├── Storage ├── AbstractStorage.php ├── Exception │ ├── ExceptionInterface.php │ ├── InvalidArgumentException.php │ ├── OutOfBoundsException.php │ └── RuntimeException.php ├── Folder.php ├── Folder │ ├── FolderInterface.php │ ├── Maildir.php │ └── Mbox.php ├── Imap.php ├── Maildir.php ├── Mbox.php ├── Message.php ├── Message │ ├── File.php │ └── MessageInterface.php ├── Part.php ├── Part │ ├── Exception │ │ ├── ExceptionInterface.php │ │ ├── InvalidArgumentException.php │ │ └── RuntimeException.php │ ├── File.php │ └── PartInterface.php ├── Pop3.php └── Writable │ ├── Maildir.php │ └── WritableInterface.php └── Transport ├── Envelope.php ├── Exception ├── DomainException.php ├── ExceptionInterface.php ├── InvalidArgumentException.php └── RuntimeException.php ├── Factory.php ├── File.php ├── FileOptions.php ├── InMemory.php ├── Null.php ├── Sendmail.php ├── Smtp.php ├── SmtpOptions.php └── TransportInterface.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2018, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-mail 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [laminas/laminas-mail](https://github.com/laminas/laminas-mail). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-mail.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-mail) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-mail/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-mail?branch=master) 9 | 10 | `Zend\Mail` provides generalized functionality to compose and send both text and 11 | MIME-compliant multipart email messages. Mail can be sent with `Zend\Mail` via 12 | the `Mail\Transport\Sendmail`, `Mail\Transport\Smtp` or the `Mail\Transport\File` 13 | transport. Of course, you can also implement your own transport by implementing 14 | the `Mail\Transport\TransportInterface`. 15 | 16 | - File issues at https://github.com/zendframework/zend-mail/issues 17 | - Documentation is at https://docs.zendframework.com/zend-mail/ 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-mail", 3 | "description": "Provides generalized functionality to compose and send both text and MIME-compliant multipart e-mail messages", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zf", 7 | "zendframework", 8 | "mail" 9 | ], 10 | "support": { 11 | "docs": "https://docs.zendframework.com/zend-mail/", 12 | "issues": "https://github.com/zendframework/zend-mail/issues", 13 | "source": "https://github.com/zendframework/zend-mail", 14 | "rss": "https://github.com/zendframework/zend-mail/releases.atom", 15 | "chat": "https://zendframework-slack.herokuapp.com", 16 | "forum": "https://discourse.zendframework.com/c/questions/components" 17 | }, 18 | "require": { 19 | "php": "^5.6 || ^7.0", 20 | "ext-iconv": "*", 21 | "zendframework/zend-loader": "^2.5", 22 | "zendframework/zend-mime": "^2.5", 23 | "zendframework/zend-stdlib": "^2.7 || ^3.0", 24 | "zendframework/zend-validator": "^2.10.2", 25 | "true/punycode": "^2.1" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^5.7.25 || ^6.4.4 || ^7.1.4", 29 | "zendframework/zend-coding-standard": "~1.0.0", 30 | "zendframework/zend-config": "^2.6", 31 | "zendframework/zend-crypt": "^2.6 || ^3.0", 32 | "zendframework/zend-servicemanager": "^2.7.10 || ^3.3.1" 33 | }, 34 | "suggest": { 35 | "zendframework/zend-crypt": "Crammd5 support in SMTP Auth", 36 | "zendframework/zend-servicemanager": "^2.7.10 || ^3.3.1 when using SMTP to deliver messages" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Zend\\Mail\\": "src/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "ZendTest\\Mail\\": "test/" 46 | } 47 | }, 48 | "config": { 49 | "sort-packages": true 50 | }, 51 | "extra": { 52 | "branch-alias": { 53 | "dev-master": "2.10.x-dev", 54 | "dev-develop": "2.11.x-dev" 55 | }, 56 | "zf": { 57 | "component": "Zend\\Mail", 58 | "config-provider": "Zend\\Mail\\ConfigProvider" 59 | } 60 | }, 61 | "scripts": { 62 | "check": [ 63 | "@cs-check", 64 | "@test" 65 | ], 66 | "cs-check": "phpcs", 67 | "cs-fix": "phpcbf", 68 | "test": "phpunit --colors=always", 69 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Address.php: -------------------------------------------------------------------------------- 1 | .*)<(?P[^>]+)>|(?P.+))$/', $address, $matches)) { 34 | throw new Exception\InvalidArgumentException('Invalid address format'); 35 | } 36 | 37 | $name = null; 38 | if (isset($matches['name'])) { 39 | $name = trim($matches['name']); 40 | } 41 | if (empty($name)) { 42 | $name = null; 43 | } 44 | 45 | if (isset($matches['namedEmail'])) { 46 | $email = $matches['namedEmail']; 47 | } 48 | if (isset($matches['email'])) { 49 | $email = $matches['email']; 50 | } 51 | $email = trim($email); 52 | 53 | return new static($email, $name, $comment); 54 | } 55 | 56 | /** 57 | * Constructor 58 | * 59 | * @param string $email 60 | * @param null|string $name 61 | * @param null|string $comment 62 | * @throws Exception\InvalidArgumentException 63 | */ 64 | public function __construct($email, $name = null, $comment = null) 65 | { 66 | $emailAddressValidator = new EmailAddressValidator(Hostname::ALLOW_DNS | Hostname::ALLOW_LOCAL); 67 | if (! is_string($email) || empty($email)) { 68 | throw new Exception\InvalidArgumentException('Email must be a valid email address'); 69 | } 70 | 71 | if (preg_match("/[\r\n]/", $email)) { 72 | throw new Exception\InvalidArgumentException('CRLF injection detected'); 73 | } 74 | 75 | if (! $emailAddressValidator->isValid($email)) { 76 | $invalidMessages = $emailAddressValidator->getMessages(); 77 | throw new Exception\InvalidArgumentException(array_shift($invalidMessages)); 78 | } 79 | 80 | if (null !== $name) { 81 | if (! is_string($name)) { 82 | throw new Exception\InvalidArgumentException('Name must be a string'); 83 | } 84 | 85 | if (preg_match("/[\r\n]/", $name)) { 86 | throw new Exception\InvalidArgumentException('CRLF injection detected'); 87 | } 88 | 89 | $this->name = $name; 90 | } 91 | 92 | $this->email = $email; 93 | 94 | if (null !== $comment) { 95 | $this->comment = $comment; 96 | } 97 | } 98 | 99 | /** 100 | * Retrieve email 101 | * 102 | * @return string 103 | */ 104 | public function getEmail() 105 | { 106 | return $this->email; 107 | } 108 | 109 | /** 110 | * Retrieve name 111 | * 112 | * @return string 113 | */ 114 | public function getName() 115 | { 116 | return $this->name; 117 | } 118 | 119 | /** 120 | * Retrieve comment, if any 121 | * 122 | * @return null|string 123 | */ 124 | public function getComment() 125 | { 126 | return $this->comment; 127 | } 128 | 129 | /** 130 | * String representation of address 131 | * 132 | * @return string 133 | */ 134 | public function toString() 135 | { 136 | $string = sprintf('<%s>', $this->getEmail()); 137 | $name = $this->constructName(); 138 | if (null === $name) { 139 | return $string; 140 | } 141 | 142 | return sprintf('%s %s', $name, $string); 143 | } 144 | 145 | /** 146 | * Constructs the name string 147 | * 148 | * If a comment is present, appends the comment (commented using parens) to 149 | * the name before returning it; otherwise, returns just the name. 150 | * 151 | * @return null|string 152 | */ 153 | private function constructName() 154 | { 155 | $name = $this->getName(); 156 | $comment = $this->getComment(); 157 | 158 | if ($comment === null || $comment === '') { 159 | return $name; 160 | } 161 | 162 | $string = sprintf('%s (%s)', $name, $comment); 163 | return trim($string); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Address/AddressInterface.php: -------------------------------------------------------------------------------- 1 | createAddress($emailOrAddress, $name); 34 | } 35 | 36 | if (! $emailOrAddress instanceof Address\AddressInterface) { 37 | throw new Exception\InvalidArgumentException(sprintf( 38 | '%s expects an email address or %s\Address object as its first argument; received "%s"', 39 | __METHOD__, 40 | __NAMESPACE__, 41 | (is_object($emailOrAddress) ? get_class($emailOrAddress) : gettype($emailOrAddress)) 42 | )); 43 | } 44 | 45 | $email = strtolower($emailOrAddress->getEmail()); 46 | if ($this->has($email)) { 47 | return $this; 48 | } 49 | 50 | $this->addresses[$email] = $emailOrAddress; 51 | return $this; 52 | } 53 | 54 | /** 55 | * Add many addresses at once 56 | * 57 | * If an email key is provided, it will be used as the email, and the value 58 | * as the name. Otherwise, the value is passed as the sole argument to add(), 59 | * and, as such, can be either email strings or Address\AddressInterface objects. 60 | * 61 | * @param array $addresses 62 | * @throws Exception\RuntimeException 63 | * @return AddressList 64 | */ 65 | public function addMany(array $addresses) 66 | { 67 | foreach ($addresses as $key => $value) { 68 | if (is_int($key) || is_numeric($key)) { 69 | $this->add($value); 70 | continue; 71 | } 72 | 73 | if (! is_string($key)) { 74 | throw new Exception\RuntimeException(sprintf( 75 | 'Invalid key type in provided addresses array ("%s")', 76 | (is_object($key) ? get_class($key) : var_export($key, 1)) 77 | )); 78 | } 79 | 80 | $this->add($key, $value); 81 | } 82 | return $this; 83 | } 84 | 85 | /** 86 | * Add an address to the list from any valid string format, such as 87 | * - "ZF Dev" 88 | * - dev@zf.com 89 | * 90 | * @param string $address 91 | * @param null|string $comment Comment associated with the address, if any. 92 | * @throws Exception\InvalidArgumentException 93 | * @return AddressList 94 | */ 95 | public function addFromString($address, $comment = null) 96 | { 97 | $this->add(Address::fromString($address, $comment)); 98 | } 99 | 100 | /** 101 | * Merge another address list into this one 102 | * 103 | * @param AddressList $addressList 104 | * @return AddressList 105 | */ 106 | public function merge(AddressList $addressList) 107 | { 108 | foreach ($addressList as $address) { 109 | $this->add($address); 110 | } 111 | return $this; 112 | } 113 | 114 | /** 115 | * Does the email exist in this list? 116 | * 117 | * @param string $email 118 | * @return bool 119 | */ 120 | public function has($email) 121 | { 122 | $email = strtolower($email); 123 | return isset($this->addresses[$email]); 124 | } 125 | 126 | /** 127 | * Get an address by email 128 | * 129 | * @param string $email 130 | * @return bool|Address\AddressInterface 131 | */ 132 | public function get($email) 133 | { 134 | $email = strtolower($email); 135 | if (! isset($this->addresses[$email])) { 136 | return false; 137 | } 138 | 139 | return $this->addresses[$email]; 140 | } 141 | 142 | /** 143 | * Delete an address from the list 144 | * 145 | * @param string $email 146 | * @return bool 147 | */ 148 | public function delete($email) 149 | { 150 | $email = strtolower($email); 151 | if (! isset($this->addresses[$email])) { 152 | return false; 153 | } 154 | 155 | unset($this->addresses[$email]); 156 | return true; 157 | } 158 | 159 | /** 160 | * Return count of addresses 161 | * 162 | * @return int 163 | */ 164 | public function count() 165 | { 166 | return count($this->addresses); 167 | } 168 | 169 | /** 170 | * Rewind iterator 171 | * 172 | * @return mixed the value of the first addresses element, or false if the addresses is 173 | * empty. 174 | * @see addresses 175 | */ 176 | public function rewind() 177 | { 178 | return reset($this->addresses); 179 | } 180 | 181 | /** 182 | * Return current item in iteration 183 | * 184 | * @return Address 185 | */ 186 | public function current() 187 | { 188 | return current($this->addresses); 189 | } 190 | 191 | /** 192 | * Return key of current item of iteration 193 | * 194 | * @return string 195 | */ 196 | public function key() 197 | { 198 | return key($this->addresses); 199 | } 200 | 201 | /** 202 | * Move to next item 203 | * 204 | * @return mixed the addresses value in the next place that's pointed to by the 205 | * internal array pointer, or false if there are no more elements. 206 | * @see addresses 207 | */ 208 | public function next() 209 | { 210 | return next($this->addresses); 211 | } 212 | 213 | /** 214 | * Is the current item of iteration valid? 215 | * 216 | * @return bool 217 | */ 218 | public function valid() 219 | { 220 | $key = key($this->addresses); 221 | return ($key !== null && $key !== false); 222 | } 223 | 224 | /** 225 | * Create an address object 226 | * 227 | * @param string $email 228 | * @param string|null $name 229 | * @return Address 230 | */ 231 | protected function createAddress($email, $name) 232 | { 233 | return new Address($email, $name); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencyConfig(), 21 | ]; 22 | } 23 | 24 | /** 25 | * Retrieve dependency settings for zend-mail package. 26 | * 27 | * @return array 28 | */ 29 | public function getDependencyConfig() 30 | { 31 | return [ 32 | 'factories' => [ 33 | Protocol\SmtpPluginManager::class => Protocol\SmtpPluginManagerFactory::class, 34 | ], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | setEncoding('UTF-8'); 95 | } 96 | 97 | /** @var AddressList $addressList */ 98 | $addressList = $header->getAddressList(); 99 | foreach ($addresses as $address) { 100 | $addressList->add($address); 101 | } 102 | 103 | return $header; 104 | } 105 | 106 | public function getFieldName() 107 | { 108 | return $this->fieldName; 109 | } 110 | 111 | /** 112 | * Safely convert UTF-8 encoded domain name to ASCII 113 | * @param string $domainName the UTF-8 encoded email 114 | * @return string 115 | */ 116 | protected function idnToAscii($domainName) 117 | { 118 | if (null === self::$punycode) { 119 | self::$punycode = new Punycode(); 120 | } 121 | try { 122 | return self::$punycode->encode($domainName); 123 | } catch (OutOfBoundsException $e) { 124 | return $domainName; 125 | } 126 | } 127 | 128 | public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 129 | { 130 | $emails = []; 131 | $encoding = $this->getEncoding(); 132 | 133 | foreach ($this->getAddressList() as $address) { 134 | $email = $address->getEmail(); 135 | $name = $address->getName(); 136 | 137 | if (! empty($name) && false !== strstr($name, ',')) { 138 | $name = sprintf('"%s"', $name); 139 | } 140 | 141 | if ($format === HeaderInterface::FORMAT_ENCODED 142 | && 'ASCII' !== $encoding 143 | ) { 144 | if (! empty($name)) { 145 | $name = HeaderWrap::mimeEncodeValue($name, $encoding); 146 | } 147 | 148 | if (preg_match('/^(.+)@([^@]+)$/', $email, $matches)) { 149 | $localPart = $matches[1]; 150 | $hostname = $this->idnToAscii($matches[2]); 151 | $email = sprintf('%s@%s', $localPart, $hostname); 152 | } 153 | } 154 | 155 | if (empty($name)) { 156 | $emails[] = $email; 157 | } else { 158 | $emails[] = sprintf('%s <%s>', $name, $email); 159 | } 160 | } 161 | 162 | // Ensure the values are valid before sending them. 163 | if ($format !== HeaderInterface::FORMAT_RAW) { 164 | foreach ($emails as $email) { 165 | HeaderValue::assertValid($email); 166 | } 167 | } 168 | 169 | return implode(',' . Headers::FOLDING, $emails); 170 | } 171 | 172 | public function setEncoding($encoding) 173 | { 174 | $this->encoding = $encoding; 175 | return $this; 176 | } 177 | 178 | public function getEncoding() 179 | { 180 | return $this->encoding; 181 | } 182 | 183 | /** 184 | * Set address list for this header 185 | * 186 | * @param AddressList $addressList 187 | */ 188 | public function setAddressList(AddressList $addressList) 189 | { 190 | $this->addressList = $addressList; 191 | } 192 | 193 | /** 194 | * Get address list managed by this header 195 | * 196 | * @return AddressList 197 | */ 198 | public function getAddressList() 199 | { 200 | if (null === $this->addressList) { 201 | $this->setAddressList(new AddressList()); 202 | } 203 | return $this->addressList; 204 | } 205 | 206 | public function toString() 207 | { 208 | $name = $this->getFieldName(); 209 | $value = $this->getFieldValue(HeaderInterface::FORMAT_ENCODED); 210 | return (empty($value)) ? '' : sprintf('%s: %s', $name, $value); 211 | } 212 | 213 | /** 214 | * Retrieve comments from value, if any. 215 | * 216 | * Supposed to be private, protected as a workaround for PHP bug 68194 217 | * 218 | * @param string $value 219 | * @return string 220 | */ 221 | protected static function getComments($value) 222 | { 223 | $matches = []; 224 | preg_match_all( 225 | '/\\( 226 | (?P( 227 | \\\\.| 228 | [^\\\\)] 229 | )+) 230 | \\)/x', 231 | $value, 232 | $matches 233 | ); 234 | return isset($matches['comment']) ? implode(', ', $matches['comment']) : ''; 235 | } 236 | 237 | /** 238 | * Strip all comments from value, if any. 239 | * 240 | * Supposed to be private, protected as a workaround for PHP bug 68194 241 | * 242 | * @param string $value 243 | * @return void 244 | */ 245 | protected static function stripComments($value) 246 | { 247 | return preg_replace( 248 | '/\\( 249 | ( 250 | \\\\.| 251 | [^\\\\)] 252 | )+ 253 | \\)/x', 254 | '', 255 | $value 256 | ); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/Header/Bcc.php: -------------------------------------------------------------------------------- 1 | setTransferEncoding($value); 51 | 52 | return $header; 53 | } 54 | 55 | public function getFieldName() 56 | { 57 | return 'Content-Transfer-Encoding'; 58 | } 59 | 60 | public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 61 | { 62 | return $this->transferEncoding; 63 | } 64 | 65 | public function setEncoding($encoding) 66 | { 67 | // Header must be always in US-ASCII 68 | return $this; 69 | } 70 | 71 | public function getEncoding() 72 | { 73 | return 'ASCII'; 74 | } 75 | 76 | public function toString() 77 | { 78 | return 'Content-Transfer-Encoding: ' . $this->getFieldValue(); 79 | } 80 | 81 | /** 82 | * Set the content transfer encoding 83 | * 84 | * @param string $transferEncoding 85 | * @throws Exception\InvalidArgumentException 86 | * @return $this 87 | */ 88 | public function setTransferEncoding($transferEncoding) 89 | { 90 | // Per RFC 1521, the value of the header is not case sensitive 91 | $transferEncoding = strtolower($transferEncoding); 92 | 93 | if (! in_array($transferEncoding, static::$allowedTransferEncodings)) { 94 | throw new Exception\InvalidArgumentException(sprintf( 95 | '%s expects one of "'. implode(', ', static::$allowedTransferEncodings) . '"; received "%s"', 96 | __METHOD__, 97 | (string) $transferEncoding 98 | )); 99 | } 100 | $this->transferEncoding = $transferEncoding; 101 | return $this; 102 | } 103 | 104 | /** 105 | * Retrieve the content transfer encoding 106 | * 107 | * @return string 108 | */ 109 | public function getTransferEncoding() 110 | { 111 | return $this->transferEncoding; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Header/ContentType.php: -------------------------------------------------------------------------------- 1 | setType($parts[0]); 47 | 48 | if (isset($parts[1])) { 49 | $values = ListParser::parse(trim($parts[1]), [';', '=']); 50 | $length = count($values); 51 | 52 | for ($i = 0; $i < $length; $i += 2) { 53 | $value = $values[$i + 1]; 54 | $value = trim($value, "'\" \t\n\r\0\x0B"); 55 | $header->addParameter($values[$i], $value); 56 | } 57 | } 58 | 59 | return $header; 60 | } 61 | 62 | public function getFieldName() 63 | { 64 | return 'Content-Type'; 65 | } 66 | 67 | public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 68 | { 69 | $prepared = $this->type; 70 | if (empty($this->parameters)) { 71 | return $prepared; 72 | } 73 | 74 | $values = [$prepared]; 75 | foreach ($this->parameters as $attribute => $value) { 76 | if (HeaderInterface::FORMAT_ENCODED === $format && ! Mime::isPrintable($value)) { 77 | $this->encoding = 'UTF-8'; 78 | $value = HeaderWrap::wrap($value, $this); 79 | $this->encoding = 'ASCII'; 80 | } 81 | 82 | $values[] = sprintf('%s="%s"', $attribute, $value); 83 | } 84 | 85 | return implode(';' . Headers::FOLDING, $values); 86 | } 87 | 88 | public function setEncoding($encoding) 89 | { 90 | $this->encoding = $encoding; 91 | return $this; 92 | } 93 | 94 | public function getEncoding() 95 | { 96 | return $this->encoding; 97 | } 98 | 99 | public function toString() 100 | { 101 | return 'Content-Type: ' . $this->getFieldValue(HeaderInterface::FORMAT_ENCODED); 102 | } 103 | 104 | /** 105 | * Set the content type 106 | * 107 | * @param string $type 108 | * @throws Exception\InvalidArgumentException 109 | * @return ContentType 110 | */ 111 | public function setType($type) 112 | { 113 | if (! preg_match('/^[a-z-]+\/[a-z0-9.+-]+$/i', $type)) { 114 | throw new Exception\InvalidArgumentException(sprintf( 115 | '%s expects a value in the format "type/subtype"; received "%s"', 116 | __METHOD__, 117 | (string) $type 118 | )); 119 | } 120 | $this->type = $type; 121 | return $this; 122 | } 123 | 124 | /** 125 | * Retrieve the content type 126 | * 127 | * @return string 128 | */ 129 | public function getType() 130 | { 131 | return $this->type; 132 | } 133 | 134 | /** 135 | * Add a parameter pair 136 | * 137 | * @param string $name 138 | * @param string $value 139 | * @return ContentType 140 | * @throws Exception\InvalidArgumentException for parameter names that do not follow RFC 2822 141 | * @throws Exception\InvalidArgumentException for parameter values that do not follow RFC 2822 142 | */ 143 | public function addParameter($name, $value) 144 | { 145 | $name = strtolower($name); 146 | $value = (string) $value; 147 | 148 | if (! HeaderValue::isValid($name)) { 149 | throw new Exception\InvalidArgumentException('Invalid content-type parameter name detected'); 150 | } 151 | if (! HeaderWrap::canBeEncoded($value)) { 152 | throw new Exception\InvalidArgumentException( 153 | 'Parameter value must be composed of printable US-ASCII or UTF-8 characters.' 154 | ); 155 | } 156 | 157 | $this->parameters[$name] = $value; 158 | return $this; 159 | } 160 | 161 | /** 162 | * Get all parameters 163 | * 164 | * @return array 165 | */ 166 | public function getParameters() 167 | { 168 | return $this->parameters; 169 | } 170 | 171 | /** 172 | * Get a parameter by name 173 | * 174 | * @param string $name 175 | * @return null|string 176 | */ 177 | public function getParameter($name) 178 | { 179 | $name = strtolower($name); 180 | if (isset($this->parameters[$name])) { 181 | return $this->parameters[$name]; 182 | } 183 | return; 184 | } 185 | 186 | /** 187 | * Remove a named parameter 188 | * 189 | * @param string $name 190 | * @return bool 191 | */ 192 | public function removeParameter($name) 193 | { 194 | $name = strtolower($name); 195 | if (isset($this->parameters[$name])) { 196 | unset($this->parameters[$name]); 197 | return true; 198 | } 199 | return false; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Header/Date.php: -------------------------------------------------------------------------------- 1 | value = $value; 41 | } 42 | 43 | public function getFieldName() 44 | { 45 | return 'Date'; 46 | } 47 | 48 | public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 49 | { 50 | return $this->value; 51 | } 52 | 53 | public function setEncoding($encoding) 54 | { 55 | // This header must be always in US-ASCII 56 | return $this; 57 | } 58 | 59 | public function getEncoding() 60 | { 61 | return 'ASCII'; 62 | } 63 | 64 | public function toString() 65 | { 66 | return 'Date: ' . $this->getFieldValue(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Header/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | setFieldName($fieldName); 81 | } 82 | 83 | if ($fieldValue !== null) { 84 | $this->setFieldValue($fieldValue); 85 | } 86 | } 87 | 88 | /** 89 | * Set header name 90 | * 91 | * @param string $fieldName 92 | * @return GenericHeader 93 | * @throws Exception\InvalidArgumentException; 94 | */ 95 | public function setFieldName($fieldName) 96 | { 97 | if (! is_string($fieldName) || empty($fieldName)) { 98 | throw new Exception\InvalidArgumentException('Header name must be a string'); 99 | } 100 | 101 | // Pre-filter to normalize valid characters, change underscore to dash 102 | $fieldName = str_replace(' ', '-', ucwords(str_replace(['_', '-'], ' ', $fieldName))); 103 | 104 | if (! HeaderName::isValid($fieldName)) { 105 | throw new Exception\InvalidArgumentException( 106 | 'Header name must be composed of printable US-ASCII characters, except colon.' 107 | ); 108 | } 109 | 110 | $this->fieldName = $fieldName; 111 | return $this; 112 | } 113 | 114 | public function getFieldName() 115 | { 116 | return $this->fieldName; 117 | } 118 | 119 | /** 120 | * Set header value 121 | * 122 | * @param string $fieldValue 123 | * @return GenericHeader 124 | * @throws Exception\InvalidArgumentException; 125 | */ 126 | public function setFieldValue($fieldValue) 127 | { 128 | $fieldValue = (string) $fieldValue; 129 | 130 | if (! HeaderWrap::canBeEncoded($fieldValue)) { 131 | throw new Exception\InvalidArgumentException( 132 | 'Header value must be composed of printable US-ASCII characters and valid folding sequences.' 133 | ); 134 | } 135 | 136 | $this->fieldValue = $fieldValue; 137 | $this->encoding = null; 138 | 139 | return $this; 140 | } 141 | 142 | public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 143 | { 144 | if (HeaderInterface::FORMAT_ENCODED === $format) { 145 | return HeaderWrap::wrap($this->fieldValue, $this); 146 | } 147 | 148 | return $this->fieldValue; 149 | } 150 | 151 | public function setEncoding($encoding) 152 | { 153 | $this->encoding = $encoding; 154 | return $this; 155 | } 156 | 157 | public function getEncoding() 158 | { 159 | if (! $this->encoding) { 160 | $this->encoding = Mime::isPrintable($this->fieldValue) ? 'ASCII' : 'UTF-8'; 161 | } 162 | 163 | return $this->encoding; 164 | } 165 | 166 | public function toString() 167 | { 168 | $name = $this->getFieldName(); 169 | if (empty($name)) { 170 | throw new Exception\RuntimeException('Header name is not set, use setFieldName()'); 171 | } 172 | $value = $this->getFieldValue(HeaderInterface::FORMAT_ENCODED); 173 | 174 | return $name . ': ' . $value; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/Header/GenericMultiHeader.php: -------------------------------------------------------------------------------- 1 | getFieldName(); 41 | $values = [$this->getFieldValue(HeaderInterface::FORMAT_ENCODED)]; 42 | 43 | foreach ($headers as $header) { 44 | if (! $header instanceof static) { 45 | throw new Exception\InvalidArgumentException( 46 | 'This method toStringMultipleHeaders was expecting an array of headers of the same type' 47 | ); 48 | } 49 | $values[] = $header->getFieldValue(HeaderInterface::FORMAT_ENCODED); 50 | } 51 | 52 | return $name . ': ' . implode(',', $values); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Header/HeaderInterface.php: -------------------------------------------------------------------------------- 1 | 'Zend\Mail\Header\Bcc', 22 | 'cc' => 'Zend\Mail\Header\Cc', 23 | 'contenttype' => 'Zend\Mail\Header\ContentType', 24 | 'content_type' => 'Zend\Mail\Header\ContentType', 25 | 'content-type' => 'Zend\Mail\Header\ContentType', 26 | 'contenttransferencoding' => 'Zend\Mail\Header\ContentTransferEncoding', 27 | 'content_transfer_encoding' => 'Zend\Mail\Header\ContentTransferEncoding', 28 | 'content-transfer-encoding' => 'Zend\Mail\Header\ContentTransferEncoding', 29 | 'date' => 'Zend\Mail\Header\Date', 30 | 'from' => 'Zend\Mail\Header\From', 31 | 'in-reply-to' => 'Zend\Mail\Header\InReplyTo', 32 | 'message-id' => 'Zend\Mail\Header\MessageId', 33 | 'mimeversion' => 'Zend\Mail\Header\MimeVersion', 34 | 'mime_version' => 'Zend\Mail\Header\MimeVersion', 35 | 'mime-version' => 'Zend\Mail\Header\MimeVersion', 36 | 'received' => 'Zend\Mail\Header\Received', 37 | 'references' => 'Zend\Mail\Header\References', 38 | 'replyto' => 'Zend\Mail\Header\ReplyTo', 39 | 'reply_to' => 'Zend\Mail\Header\ReplyTo', 40 | 'reply-to' => 'Zend\Mail\Header\ReplyTo', 41 | 'sender' => 'Zend\Mail\Header\Sender', 42 | 'subject' => 'Zend\Mail\Header\Subject', 43 | 'to' => 'Zend\Mail\Header\To', 44 | ]; 45 | } 46 | -------------------------------------------------------------------------------- /src/Header/HeaderName.php: -------------------------------------------------------------------------------- 1 | 32 && $ord < 127 && $ord !== 58) { 33 | $result .= $name[$i]; 34 | } 35 | } 36 | return $result; 37 | } 38 | 39 | /** 40 | * Determine if the header name contains any invalid characters. 41 | * 42 | * @param string $name 43 | * @return bool 44 | */ 45 | public static function isValid($name) 46 | { 47 | $tot = strlen($name); 48 | for ($i = 0; $i < $tot; $i += 1) { 49 | $ord = ord($name[$i]); 50 | if ($ord < 33 || $ord > 126 || $ord === 58) { 51 | return false; 52 | } 53 | } 54 | return true; 55 | } 56 | 57 | /** 58 | * Assert that the header name is valid. 59 | * 60 | * Raises an exception if invalid. 61 | * 62 | * @param string $name 63 | * @throws Exception\RuntimeException 64 | * @return void 65 | */ 66 | public static function assertValid($name) 67 | { 68 | if (! self::isValid($name)) { 69 | throw new Exception\RuntimeException('Invalid header name detected'); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Header/HeaderValue.php: -------------------------------------------------------------------------------- 1 | 127) { 36 | continue; 37 | } 38 | 39 | if ($ord === 13) { 40 | if ($i + 2 >= $total) { 41 | continue; 42 | } 43 | 44 | $lf = ord($value[$i + 1]); 45 | $sp = ord($value[$i + 2]); 46 | 47 | if ($lf !== 10 || $sp !== 32) { 48 | continue; 49 | } 50 | 51 | $result .= "\r\n "; 52 | $i += 2; 53 | continue; 54 | } 55 | 56 | $result .= $value[$i]; 57 | } 58 | 59 | return $result; 60 | } 61 | 62 | /** 63 | * Determine if the header value contains any invalid characters. 64 | * 65 | * @see http://www.rfc-base.org/txt/rfc-2822.txt (section 2.2) 66 | * @param string $value 67 | * @return bool 68 | */ 69 | public static function isValid($value) 70 | { 71 | $total = strlen($value); 72 | for ($i = 0; $i < $total; $i += 1) { 73 | $ord = ord($value[$i]); 74 | 75 | // bare LF means we aren't valid 76 | if ($ord === 10 || $ord > 127) { 77 | return false; 78 | } 79 | 80 | if ($ord === 13) { 81 | if ($i + 2 >= $total) { 82 | return false; 83 | } 84 | 85 | $lf = ord($value[$i + 1]); 86 | $sp = ord($value[$i + 2]); 87 | 88 | if ($lf !== 10 || ! in_array($sp, [9, 32], true)) { 89 | return false; 90 | } 91 | 92 | // skip over the LF following this 93 | $i += 2; 94 | } 95 | } 96 | 97 | return true; 98 | } 99 | 100 | /** 101 | * Assert that the header value is valid. 102 | * 103 | * Raises an exception if invalid. 104 | * 105 | * @param string $value 106 | * @throws Exception\RuntimeException 107 | * @return void 108 | */ 109 | public static function assertValid($value) 110 | { 111 | if (! self::isValid($value)) { 112 | throw new Exception\RuntimeException('Invalid header value detected'); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Header/HeaderWrap.php: -------------------------------------------------------------------------------- 1 | getEncoding(); 48 | if ($encoding == 'ASCII') { 49 | return wordwrap($value, 78, Headers::FOLDING); 50 | } 51 | return static::mimeEncodeValue($value, $encoding, 78); 52 | } 53 | 54 | /** 55 | * Wrap a structured header line 56 | * 57 | * @param string $value 58 | * @param StructuredInterface $header 59 | * @return string 60 | */ 61 | protected static function wrapStructuredHeader($value, StructuredInterface $header) 62 | { 63 | $delimiter = $header->getDelimiter(); 64 | 65 | $length = strlen($value); 66 | $lines = []; 67 | $temp = ''; 68 | for ($i = 0; $i < $length; $i++) { 69 | $temp .= $value[$i]; 70 | if ($value[$i] == $delimiter) { 71 | $lines[] = $temp; 72 | $temp = ''; 73 | } 74 | } 75 | return implode(Headers::FOLDING, $lines); 76 | } 77 | 78 | /** 79 | * MIME-encode a value 80 | * 81 | * Performs quoted-printable encoding on a value, setting maximum 82 | * line-length to 998. 83 | * 84 | * @param string $value 85 | * @param string $encoding 86 | * @param int $lineLength maximum line-length, by default 998 87 | * @return string Returns the mime encode value without the last line ending 88 | */ 89 | public static function mimeEncodeValue($value, $encoding, $lineLength = 998) 90 | { 91 | return Mime::encodeQuotedPrintableHeader($value, $encoding, $lineLength, Headers::EOL); 92 | } 93 | 94 | /** 95 | * MIME-decode a value 96 | * 97 | * Performs quoted-printable decoding on a value. 98 | * 99 | * @param string $value 100 | * @return string Returns the mime encode value without the last line ending 101 | */ 102 | public static function mimeDecodeValue($value) 103 | { 104 | // unfold first, because iconv_mime_decode is discarding "\n" with no apparent reason 105 | // making the resulting value no longer valid. 106 | 107 | // see https://tools.ietf.org/html/rfc2822#section-2.2.3 about unfolding 108 | $parts = explode(Headers::FOLDING, $value); 109 | $value = implode(' ', $parts); 110 | 111 | $decodedValue = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8'); 112 | 113 | // imap (unlike iconv) can handle multibyte headers which are splitted across multiple line 114 | if (self::isNotDecoded($value, $decodedValue) && extension_loaded('imap')) { 115 | return array_reduce( 116 | imap_mime_header_decode(imap_utf8($value)), 117 | function ($accumulator, $headerPart) { 118 | return $accumulator . $headerPart->text; 119 | }, 120 | '' 121 | ); 122 | } 123 | 124 | return $decodedValue; 125 | } 126 | 127 | private static function isNotDecoded($originalValue, $value) 128 | { 129 | return 0 === strpos($value, '=?') 130 | && strlen($value) - 2 === strpos($value, '?=') 131 | && false !== strpos($originalValue, $value); 132 | } 133 | 134 | /** 135 | * Test if is possible apply MIME-encoding 136 | * 137 | * @param string $value 138 | * @return bool 139 | */ 140 | public static function canBeEncoded($value) 141 | { 142 | // avoid any wrapping by specifying line length long enough 143 | // "test" -> 4 144 | // "x-test: =?ISO-8859-1?B?dGVzdA==?=" -> 33 145 | // 8 +2 +3 +3 -> 16 146 | $charset = 'UTF-8'; 147 | $lineLength = strlen($value) * 4 + strlen($charset) + 16; 148 | 149 | $preferences = [ 150 | 'scheme' => 'Q', 151 | 'input-charset' => $charset, 152 | 'output-charset' => $charset, 153 | 'line-length' => $lineLength, 154 | ]; 155 | 156 | $encoded = iconv_mime_encode('x-test', $value, $preferences); 157 | 158 | return (false !== $encoded); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Header/IdentificationField.php: -------------------------------------------------------------------------------- 1 | setIds($messageIds); 55 | 56 | return $header; 57 | } 58 | 59 | /** 60 | * @param string $id 61 | * @return string 62 | */ 63 | private static function trimMessageId($id) 64 | { 65 | return trim($id, "\t\n\r\0\x0B<>"); 66 | } 67 | 68 | /** 69 | * @return string 70 | */ 71 | public function getFieldName() 72 | { 73 | return $this->fieldName; 74 | } 75 | 76 | /** 77 | * @param bool $format 78 | * @return string 79 | */ 80 | public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 81 | { 82 | return implode(Headers::FOLDING, array_map(function ($id) { 83 | return sprintf('<%s>', $id); 84 | }, $this->messageIds)); 85 | } 86 | 87 | /** 88 | * @param string $encoding Ignored; headers of this type MUST always be in 89 | * ASCII. 90 | * @return static This method is a no-op, and implements a fluent interface. 91 | */ 92 | public function setEncoding($encoding) 93 | { 94 | return $this; 95 | } 96 | 97 | /** 98 | * @return string Always returns ASCII 99 | */ 100 | public function getEncoding() 101 | { 102 | return 'ASCII'; 103 | } 104 | 105 | /** 106 | * @return string 107 | */ 108 | public function toString() 109 | { 110 | return sprintf('%s: %s', $this->fieldName, $this->getFieldValue()); 111 | } 112 | 113 | /** 114 | * Set the message ids 115 | * 116 | * @param string[] $ids 117 | * @return static This method implements a fluent interface. 118 | */ 119 | public function setIds($ids) 120 | { 121 | foreach ($ids as $id) { 122 | if (! HeaderValue::isValid($id) 123 | || preg_match("/[\r\n]/", $id) 124 | ) { 125 | throw new Exception\InvalidArgumentException('Invalid ID detected'); 126 | } 127 | } 128 | 129 | $this->messageIds = array_map([IdentificationField::class, "trimMessageId"], $ids); 130 | return $this; 131 | } 132 | 133 | /** 134 | * Retrieve the message ids 135 | * 136 | * @return string[] 137 | */ 138 | public function getIds() 139 | { 140 | return $this->messageIds; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Header/InReplyTo.php: -------------------------------------------------------------------------------- 1 | setId($value); 29 | 30 | return $header; 31 | } 32 | 33 | public function getFieldName() 34 | { 35 | return 'Message-ID'; 36 | } 37 | 38 | public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 39 | { 40 | return $this->messageId; 41 | } 42 | 43 | public function setEncoding($encoding) 44 | { 45 | // This header must be always in US-ASCII 46 | return $this; 47 | } 48 | 49 | public function getEncoding() 50 | { 51 | return 'ASCII'; 52 | } 53 | 54 | public function toString() 55 | { 56 | return 'Message-ID: ' . $this->getFieldValue(); 57 | } 58 | 59 | /** 60 | * Set the message id 61 | * 62 | * @param string|null $id 63 | * @return MessageId 64 | */ 65 | public function setId($id = null) 66 | { 67 | if ($id === null) { 68 | $id = $this->createMessageId(); 69 | } else { 70 | $id = trim($id, '<>'); 71 | } 72 | 73 | if (! HeaderValue::isValid($id) 74 | || preg_match("/[\r\n]/", $id) 75 | ) { 76 | throw new Exception\InvalidArgumentException('Invalid ID detected'); 77 | } 78 | 79 | $this->messageId = sprintf('<%s>', $id); 80 | return $this; 81 | } 82 | 83 | /** 84 | * Retrieve the message id 85 | * 86 | * @return string 87 | */ 88 | public function getId() 89 | { 90 | return $this->messageId; 91 | } 92 | 93 | /** 94 | * Creates the Message-ID 95 | * 96 | * @return string 97 | */ 98 | public function createMessageId() 99 | { 100 | $time = time(); 101 | 102 | if (isset($_SERVER['REMOTE_ADDR'])) { 103 | $user = $_SERVER['REMOTE_ADDR']; 104 | } else { 105 | $user = getmypid(); 106 | } 107 | 108 | $rand = mt_rand(); 109 | 110 | if (isset($_SERVER["SERVER_NAME"])) { 111 | $hostName = $_SERVER["SERVER_NAME"]; 112 | } else { 113 | $hostName = php_uname('n'); 114 | } 115 | 116 | return sha1($time . $user . $rand) . '@' . $hostName; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Header/MimeVersion.php: -------------------------------------------------------------------------------- 1 | \d+\.\d+)$/', $value, $matches)) { 30 | $header->setVersion($matches['version']); 31 | } 32 | 33 | return $header; 34 | } 35 | 36 | public function getFieldName() 37 | { 38 | return 'MIME-Version'; 39 | } 40 | 41 | public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 42 | { 43 | return $this->version; 44 | } 45 | 46 | public function setEncoding($encoding) 47 | { 48 | // This header must be always in US-ASCII 49 | return $this; 50 | } 51 | 52 | public function getEncoding() 53 | { 54 | return 'ASCII'; 55 | } 56 | 57 | public function toString() 58 | { 59 | return 'MIME-Version: ' . $this->getFieldValue(); 60 | } 61 | 62 | /** 63 | * Set the version string used in this header 64 | * 65 | * @param string $version 66 | * @return MimeVersion 67 | */ 68 | public function setVersion($version) 69 | { 70 | if (! preg_match('/^[1-9]\d*\.\d+$/', $version)) { 71 | throw new Exception\InvalidArgumentException('Invalid MIME-Version value detected'); 72 | } 73 | $this->version = $version; 74 | return $this; 75 | } 76 | 77 | /** 78 | * Retrieve the version string for this header 79 | * 80 | * @return string 81 | */ 82 | public function getVersion() 83 | { 84 | return $this->version; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Header/MultipleHeadersInterface.php: -------------------------------------------------------------------------------- 1 | value = $value; 43 | } 44 | 45 | public function getFieldName() 46 | { 47 | return 'Received'; 48 | } 49 | 50 | public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 51 | { 52 | return $this->value; 53 | } 54 | 55 | public function setEncoding($encoding) 56 | { 57 | // This header must be always in US-ASCII 58 | return $this; 59 | } 60 | 61 | public function getEncoding() 62 | { 63 | return 'ASCII'; 64 | } 65 | 66 | public function toString() 67 | { 68 | return 'Received: ' . $this->getFieldValue(); 69 | } 70 | 71 | /** 72 | * Serialize collection of Received headers to string 73 | * 74 | * @param array $headers 75 | * @throws Exception\RuntimeException 76 | * @return string 77 | */ 78 | public function toStringMultipleHeaders(array $headers) 79 | { 80 | $strings = [$this->toString()]; 81 | foreach ($headers as $header) { 82 | if (! $header instanceof Received) { 83 | throw new Exception\RuntimeException( 84 | 'The Received multiple header implementation can only accept an array of Received headers' 85 | ); 86 | } 87 | $strings[] = $header->toString(); 88 | } 89 | return implode(Headers::EOL, $strings); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Header/References.php: -------------------------------------------------------------------------------- 1 | when a name is present 47 | * 'name' and 'email' capture groups correspond respectively to 'display-name' and 'addr-spec' in the ABNF 48 | * @see https://tools.ietf.org/html/rfc5322#section-3.4 49 | */ 50 | $hasMatches = preg_match( 51 | '/^(?:(?P.+)\s)?(?(name)<|[^\s]+?)(?(name)>|>?)$/', 52 | $value, 53 | $matches 54 | ); 55 | 56 | if ($hasMatches !== 1) { 57 | throw new Exception\InvalidArgumentException('Invalid header value for Sender string'); 58 | } 59 | 60 | $senderName = trim($matches['name']); 61 | 62 | if (empty($senderName)) { 63 | $senderName = null; 64 | } 65 | 66 | $header->setAddress($matches['email'], $senderName); 67 | 68 | return $header; 69 | } 70 | 71 | public function getFieldName() 72 | { 73 | return 'Sender'; 74 | } 75 | 76 | public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 77 | { 78 | if (! $this->address instanceof Mail\Address\AddressInterface) { 79 | return ''; 80 | } 81 | 82 | $email = sprintf('<%s>', $this->address->getEmail()); 83 | $name = $this->address->getName(); 84 | 85 | if (! empty($name)) { 86 | if ($format == HeaderInterface::FORMAT_ENCODED) { 87 | $encoding = $this->getEncoding(); 88 | if ('ASCII' !== $encoding) { 89 | $name = HeaderWrap::mimeEncodeValue($name, $encoding); 90 | } 91 | } 92 | $email = sprintf('%s %s', $name, $email); 93 | } 94 | 95 | return $email; 96 | } 97 | 98 | public function setEncoding($encoding) 99 | { 100 | $this->encoding = $encoding; 101 | return $this; 102 | } 103 | 104 | public function getEncoding() 105 | { 106 | if (! $this->encoding) { 107 | $this->encoding = Mime::isPrintable($this->getFieldValue(HeaderInterface::FORMAT_RAW)) 108 | ? 'ASCII' 109 | : 'UTF-8'; 110 | } 111 | 112 | return $this->encoding; 113 | } 114 | 115 | public function toString() 116 | { 117 | return 'Sender: ' . $this->getFieldValue(HeaderInterface::FORMAT_ENCODED); 118 | } 119 | 120 | /** 121 | * Set the address used in this header 122 | * 123 | * @param string|\Zend\Mail\Address\AddressInterface $emailOrAddress 124 | * @param null|string $name 125 | * @throws Exception\InvalidArgumentException 126 | * @return Sender 127 | */ 128 | public function setAddress($emailOrAddress, $name = null) 129 | { 130 | if (is_string($emailOrAddress)) { 131 | $emailOrAddress = new Mail\Address($emailOrAddress, $name); 132 | } elseif (! $emailOrAddress instanceof Mail\Address\AddressInterface) { 133 | throw new Exception\InvalidArgumentException(sprintf( 134 | '%s expects a string or AddressInterface object; received "%s"', 135 | __METHOD__, 136 | (is_object($emailOrAddress) ? get_class($emailOrAddress) : gettype($emailOrAddress)) 137 | )); 138 | } 139 | $this->address = $emailOrAddress; 140 | return $this; 141 | } 142 | 143 | /** 144 | * Retrieve the internal address from this header 145 | * 146 | * @return \Zend\Mail\Address\AddressInterface|null 147 | */ 148 | public function getAddress() 149 | { 150 | return $this->address; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Header/StructuredInterface.php: -------------------------------------------------------------------------------- 1 | setSubject($value); 44 | 45 | return $header; 46 | } 47 | 48 | public function getFieldName() 49 | { 50 | return 'Subject'; 51 | } 52 | 53 | public function getFieldValue($format = HeaderInterface::FORMAT_RAW) 54 | { 55 | if (HeaderInterface::FORMAT_ENCODED === $format) { 56 | return HeaderWrap::wrap($this->subject, $this); 57 | } 58 | 59 | return $this->subject; 60 | } 61 | 62 | public function setEncoding($encoding) 63 | { 64 | $this->encoding = $encoding; 65 | return $this; 66 | } 67 | 68 | public function getEncoding() 69 | { 70 | if (! $this->encoding) { 71 | $this->encoding = Mime::isPrintable($this->subject) ? 'ASCII' : 'UTF-8'; 72 | } 73 | 74 | return $this->encoding; 75 | } 76 | 77 | public function setSubject($subject) 78 | { 79 | $subject = (string) $subject; 80 | 81 | if (! HeaderWrap::canBeEncoded($subject)) { 82 | throw new Exception\InvalidArgumentException( 83 | 'Subject value must be composed of printable US-ASCII or UTF-8 characters.' 84 | ); 85 | } 86 | 87 | $this->subject = $subject; 88 | $this->encoding = null; 89 | 90 | return $this; 91 | } 92 | 93 | public function toString() 94 | { 95 | return 'Subject: ' . $this->getFieldValue(HeaderInterface::FORMAT_ENCODED); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Header/To.php: -------------------------------------------------------------------------------- 1 | $value) { 31 | $setter = self::getSetterMethod($key); 32 | if (method_exists($message, $setter)) { 33 | $message->{$setter}($value); 34 | } 35 | } 36 | 37 | return $message; 38 | } 39 | 40 | /** 41 | * Generate a setter method name based on a provided key. 42 | * 43 | * @param string $key 44 | * @return string 45 | */ 46 | private static function getSetterMethod($key) 47 | { 48 | return 'set' 49 | . str_replace( 50 | ' ', 51 | '', 52 | ucwords( 53 | strtr( 54 | $key, 55 | [ 56 | '-' => ' ', 57 | '_' => ' ', 58 | ] 59 | ) 60 | ) 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | $provider->getDependencyConfig(), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Protocol/AbstractProtocol.php: -------------------------------------------------------------------------------- 1 | validHost = new Validator\ValidatorChain(); 88 | $this->validHost->attach(new Validator\Hostname(Validator\Hostname::ALLOW_ALL)); 89 | 90 | if (! $this->validHost->isValid($host)) { 91 | throw new Exception\RuntimeException(implode(', ', $this->validHost->getMessages())); 92 | } 93 | 94 | $this->host = $host; 95 | $this->port = $port; 96 | } 97 | 98 | /** 99 | * Class destructor to cleanup open resources 100 | * 101 | */ 102 | public function __destruct() 103 | { 104 | $this->_disconnect(); 105 | } 106 | 107 | /** 108 | * Set the maximum log size 109 | * 110 | * @param int $maximumLog Maximum log size 111 | */ 112 | public function setMaximumLog($maximumLog) 113 | { 114 | $this->maximumLog = (int) $maximumLog; 115 | } 116 | 117 | /** 118 | * Get the maximum log size 119 | * 120 | * @return int the maximum log size 121 | */ 122 | public function getMaximumLog() 123 | { 124 | return $this->maximumLog; 125 | } 126 | 127 | /** 128 | * Create a connection to the remote host 129 | * 130 | * Concrete adapters for this class will implement their own unique connect 131 | * scripts, using the _connect() method to create the socket resource. 132 | */ 133 | abstract public function connect(); 134 | 135 | /** 136 | * Retrieve the last client request 137 | * 138 | * @return string 139 | */ 140 | public function getRequest() 141 | { 142 | return $this->request; 143 | } 144 | 145 | /** 146 | * Retrieve the last server response 147 | * 148 | * @return array 149 | */ 150 | public function getResponse() 151 | { 152 | return $this->response; 153 | } 154 | 155 | /** 156 | * Retrieve the transaction log 157 | * 158 | * @return string 159 | */ 160 | public function getLog() 161 | { 162 | return implode('', $this->log); 163 | } 164 | 165 | /** 166 | * Reset the transaction log 167 | * 168 | */ 169 | public function resetLog() 170 | { 171 | $this->log = []; 172 | } 173 | 174 | // @codingStandardsIgnoreStart 175 | /** 176 | * Add the transaction log 177 | * 178 | * @param string $value new transaction 179 | */ 180 | protected function _addLog($value) 181 | { 182 | // @codingStandardsIgnoreEnd 183 | if ($this->maximumLog >= 0 && count($this->log) >= $this->maximumLog) { 184 | array_shift($this->log); 185 | } 186 | 187 | $this->log[] = $value; 188 | } 189 | 190 | // @codingStandardsIgnoreStart 191 | /** 192 | * Connect to the server using the supplied transport and target 193 | * 194 | * An example $remote string may be 'tcp://mail.example.com:25' or 'ssh://hostname.com:2222' 195 | * 196 | * @param string $remote Remote 197 | * @throws Exception\RuntimeException 198 | * @return bool 199 | */ 200 | protected function _connect($remote) 201 | { 202 | // @codingStandardsIgnoreEnd 203 | $errorNum = 0; 204 | $errorStr = ''; 205 | 206 | // open connection 207 | set_error_handler( 208 | function ($error, $message = '') { 209 | throw new Exception\RuntimeException(sprintf('Could not open socket: %s', $message), $error); 210 | }, 211 | E_WARNING 212 | ); 213 | $this->socket = stream_socket_client($remote, $errorNum, $errorStr, self::TIMEOUT_CONNECTION); 214 | restore_error_handler(); 215 | 216 | if ($this->socket === false) { 217 | if ($errorNum == 0) { 218 | $errorStr = 'Could not open socket'; 219 | } 220 | throw new Exception\RuntimeException($errorStr); 221 | } 222 | 223 | if (($result = stream_set_timeout($this->socket, self::TIMEOUT_CONNECTION)) === false) { 224 | throw new Exception\RuntimeException('Could not set stream timeout'); 225 | } 226 | 227 | return $result; 228 | } 229 | 230 | // @codingStandardsIgnoreStart 231 | /** 232 | * Disconnect from remote host and free resource 233 | * 234 | */ 235 | protected function _disconnect() 236 | { 237 | // @codingStandardsIgnoreEnd 238 | if (is_resource($this->socket)) { 239 | fclose($this->socket); 240 | } 241 | } 242 | 243 | // @codingStandardsIgnoreStart 244 | /** 245 | * Send the given request followed by a LINEEND to the server. 246 | * 247 | * @param string $request 248 | * @throws Exception\RuntimeException 249 | * @return int|bool Number of bytes written to remote host 250 | */ 251 | protected function _send($request) 252 | { 253 | // @codingStandardsIgnoreEnd 254 | if (! is_resource($this->socket)) { 255 | throw new Exception\RuntimeException('No connection has been established to ' . $this->host); 256 | } 257 | 258 | $this->request = $request; 259 | 260 | $result = fwrite($this->socket, $request . self::EOL); 261 | 262 | // Save request to internal log 263 | $this->_addLog($request . self::EOL); 264 | 265 | if ($result === false) { 266 | throw new Exception\RuntimeException('Could not send request to ' . $this->host); 267 | } 268 | 269 | return $result; 270 | } 271 | 272 | // @codingStandardsIgnoreStart 273 | /** 274 | * Get a line from the stream. 275 | * 276 | * @param int $timeout Per-request timeout value if applicable 277 | * @throws Exception\RuntimeException 278 | * @return string 279 | */ 280 | protected function _receive($timeout = null) 281 | { 282 | // @codingStandardsIgnoreEnd 283 | if (! is_resource($this->socket)) { 284 | throw new Exception\RuntimeException('No connection has been established to ' . $this->host); 285 | } 286 | 287 | // Adapters may wish to supply per-commend timeouts according to appropriate RFC 288 | if ($timeout !== null) { 289 | stream_set_timeout($this->socket, $timeout); 290 | } 291 | 292 | // Retrieve response 293 | $response = fgets($this->socket, 1024); 294 | 295 | // Save request to internal log 296 | $this->_addLog($response); 297 | 298 | // Check meta data to ensure connection is still valid 299 | $info = stream_get_meta_data($this->socket); 300 | 301 | if (! empty($info['timed_out'])) { 302 | throw new Exception\RuntimeException($this->host . ' has timed out'); 303 | } 304 | 305 | if ($response === false) { 306 | throw new Exception\RuntimeException('Could not read from ' . $this->host); 307 | } 308 | 309 | return $response; 310 | } 311 | 312 | // @codingStandardsIgnoreStart 313 | /** 314 | * Parse server response for successful codes 315 | * 316 | * Read the response from the stream and check for expected return code. 317 | * Throws a Zend\Mail\Protocol\Exception\ExceptionInterface if an unexpected code is returned. 318 | * 319 | * @param string|array $code One or more codes that indicate a successful response 320 | * @param int $timeout Per-request timeout value if applicable 321 | * @throws Exception\RuntimeException 322 | * @return string Last line of response string 323 | */ 324 | protected function _expect($code, $timeout = null) 325 | { 326 | // @codingStandardsIgnoreEnd 327 | $this->response = []; 328 | $errMsg = ''; 329 | 330 | if (! is_array($code)) { 331 | $code = [$code]; 332 | } 333 | 334 | do { 335 | $this->response[] = $result = $this->_receive($timeout); 336 | list($cmd, $more, $msg) = preg_split('/([\s-]+)/', $result, 2, PREG_SPLIT_DELIM_CAPTURE); 337 | 338 | if ($errMsg !== '') { 339 | $errMsg .= ' ' . $msg; 340 | } elseif ($cmd === null || ! in_array($cmd, $code)) { 341 | $errMsg = $msg; 342 | } 343 | 344 | // The '-' message prefix indicates an information string instead of a response string. 345 | } while (strpos($more, '-') === 0); 346 | 347 | if ($errMsg !== '') { 348 | throw new Exception\RuntimeException($errMsg); 349 | } 350 | 351 | return $msg; 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/Protocol/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | setUsername($config['username']); 54 | } 55 | if (isset($config['password'])) { 56 | $this->setPassword($config['password']); 57 | } 58 | } 59 | 60 | // Call parent with original arguments 61 | parent::__construct($host, $port, $origConfig); 62 | } 63 | 64 | /** 65 | * Performs CRAM-MD5 authentication with supplied credentials 66 | */ 67 | public function auth() 68 | { 69 | // Ensure AUTH has not already been initiated. 70 | parent::auth(); 71 | 72 | $this->_send('AUTH CRAM-MD5'); 73 | $challenge = $this->_expect(334); 74 | $challenge = base64_decode($challenge); 75 | $digest = $this->hmacMd5($this->getPassword(), $challenge); 76 | $this->_send(base64_encode($this->getUsername() . ' ' . $digest)); 77 | $this->_expect(235); 78 | $this->auth = true; 79 | } 80 | 81 | /** 82 | * Set value for username 83 | * 84 | * @param string $username 85 | * @return Crammd5 86 | */ 87 | public function setUsername($username) 88 | { 89 | $this->username = $username; 90 | return $this; 91 | } 92 | 93 | /** 94 | * Get username 95 | * 96 | * @return string 97 | */ 98 | public function getUsername() 99 | { 100 | return $this->username; 101 | } 102 | 103 | /** 104 | * Set value for password 105 | * 106 | * @param string $password 107 | * @return Crammd5 108 | */ 109 | public function setPassword($password) 110 | { 111 | $this->password = $password; 112 | return $this; 113 | } 114 | 115 | /** 116 | * Get password 117 | * 118 | * @return string 119 | */ 120 | public function getPassword() 121 | { 122 | return $this->password; 123 | } 124 | 125 | /** 126 | * Prepare CRAM-MD5 response to server's ticket 127 | * 128 | * @param string $key Challenge key (usually password) 129 | * @param string $data Challenge data 130 | * @param int $block Length of blocks (deprecated; unused) 131 | * @return string 132 | */ 133 | protected function hmacMd5($key, $data, $block = 64) 134 | { 135 | return Hmac::compute($key, 'md5', $data); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Protocol/Smtp/Auth/Login.php: -------------------------------------------------------------------------------- 1 | setUsername($config['username']); 54 | } 55 | if (isset($config['password'])) { 56 | $this->setPassword($config['password']); 57 | } 58 | } 59 | 60 | // Call parent with original arguments 61 | parent::__construct($host, $port, $origConfig); 62 | } 63 | 64 | /** 65 | * Perform LOGIN authentication with supplied credentials 66 | * 67 | */ 68 | public function auth() 69 | { 70 | // Ensure AUTH has not already been initiated. 71 | parent::auth(); 72 | 73 | $this->_send('AUTH LOGIN'); 74 | $this->_expect(334); 75 | $this->_send(base64_encode($this->getUsername())); 76 | $this->_expect(334); 77 | $this->_send(base64_encode($this->getPassword())); 78 | $this->_expect(235); 79 | $this->auth = true; 80 | } 81 | 82 | /** 83 | * Set value for username 84 | * 85 | * @param string $username 86 | * @return Login 87 | */ 88 | public function setUsername($username) 89 | { 90 | $this->username = $username; 91 | return $this; 92 | } 93 | 94 | /** 95 | * Get username 96 | * 97 | * @return string 98 | */ 99 | public function getUsername() 100 | { 101 | return $this->username; 102 | } 103 | 104 | /** 105 | * Set value for password 106 | * 107 | * @param string $password 108 | * @return Login 109 | */ 110 | public function setPassword($password) 111 | { 112 | $this->password = $password; 113 | return $this; 114 | } 115 | 116 | /** 117 | * Get password 118 | * 119 | * @return string 120 | */ 121 | public function getPassword() 122 | { 123 | return $this->password; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Protocol/Smtp/Auth/Plain.php: -------------------------------------------------------------------------------- 1 | setUsername($config['username']); 54 | } 55 | if (isset($config['password'])) { 56 | $this->setPassword($config['password']); 57 | } 58 | } 59 | 60 | // Call parent with original arguments 61 | parent::__construct($host, $port, $origConfig); 62 | } 63 | 64 | /** 65 | * Perform PLAIN authentication with supplied credentials 66 | * 67 | */ 68 | public function auth() 69 | { 70 | // Ensure AUTH has not already been initiated. 71 | parent::auth(); 72 | 73 | $this->_send('AUTH PLAIN'); 74 | $this->_expect(334); 75 | $this->_send(base64_encode("\0" . $this->getUsername() . "\0" . $this->getPassword())); 76 | $this->_expect(235); 77 | $this->auth = true; 78 | } 79 | 80 | /** 81 | * Set value for username 82 | * 83 | * @param string $username 84 | * @return Plain 85 | */ 86 | public function setUsername($username) 87 | { 88 | $this->username = $username; 89 | return $this; 90 | } 91 | 92 | /** 93 | * Get username 94 | * 95 | * @return string 96 | */ 97 | public function getUsername() 98 | { 99 | return $this->username; 100 | } 101 | 102 | /** 103 | * Set value for password 104 | * 105 | * @param string $password 106 | * @return Plain 107 | */ 108 | public function setPassword($password) 109 | { 110 | $this->password = $password; 111 | return $this; 112 | } 113 | 114 | /** 115 | * Get password 116 | * 117 | * @return string 118 | */ 119 | public function getPassword() 120 | { 121 | return $this->password; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Protocol/SmtpPluginManager.php: -------------------------------------------------------------------------------- 1 | Smtp\Auth\Crammd5::class, 27 | 'cramMd5' => Smtp\Auth\Crammd5::class, 28 | 'CramMd5' => Smtp\Auth\Crammd5::class, 29 | 'cramMD5' => Smtp\Auth\Crammd5::class, 30 | 'CramMD5' => Smtp\Auth\Crammd5::class, 31 | 'login' => Smtp\Auth\Login::class, 32 | 'Login' => Smtp\Auth\Login::class, 33 | 'plain' => Smtp\Auth\Plain::class, 34 | 'Plain' => Smtp\Auth\Plain::class, 35 | 'smtp' => Smtp::class, 36 | 'Smtp' => Smtp::class, 37 | 'SMTP' => Smtp::class, 38 | ]; 39 | 40 | /** 41 | * Service factories 42 | * 43 | * @var array 44 | */ 45 | protected $factories = [ 46 | Smtp\Auth\Crammd5::class => InvokableFactory::class, 47 | Smtp\Auth\Login::class => InvokableFactory::class, 48 | Smtp\Auth\Plain::class => InvokableFactory::class, 49 | Smtp::class => InvokableFactory::class, 50 | 51 | // v2 normalized service names 52 | 53 | 'zendmailprotocolsmtpauthcrammd5' => InvokableFactory::class, 54 | 'zendmailprotocolsmtpauthlogin' => InvokableFactory::class, 55 | 'zendmailprotocolsmtpauthplain' => InvokableFactory::class, 56 | 'zendmailprotocolsmtp' => InvokableFactory::class, 57 | ]; 58 | 59 | /** 60 | * Plugins must be an instance of the Smtp class 61 | * 62 | * @var string 63 | */ 64 | protected $instanceOf = Smtp::class; 65 | 66 | /** 67 | * Validate a retrieved plugin instance (v3). 68 | * 69 | * @param object $plugin 70 | * @throws InvalidServiceException 71 | */ 72 | public function validate($plugin) 73 | { 74 | if (! $plugin instanceof $this->instanceOf) { 75 | throw new InvalidServiceException(sprintf( 76 | 'Plugin of type %s is invalid; must extend %s', 77 | (is_object($plugin) ? get_class($plugin) : gettype($plugin)), 78 | Smtp::class 79 | )); 80 | } 81 | } 82 | 83 | /** 84 | * Validate a retrieved plugin instance (v2). 85 | * 86 | * @param object $plugin 87 | * @throws Exception\InvalidArgumentException 88 | */ 89 | public function validatePlugin($plugin) 90 | { 91 | try { 92 | $this->validate($plugin); 93 | } catch (InvalidServiceException $e) { 94 | throw new Exception\InvalidArgumentException( 95 | $e->getMessage(), 96 | $e->getCode(), 97 | $e 98 | ); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Protocol/SmtpPluginManagerFactory.php: -------------------------------------------------------------------------------- 1 | creationOptions); 41 | } 42 | 43 | /** 44 | * zend-servicemanager v2 support for invocation options. 45 | * 46 | * @param array $options 47 | * @return void 48 | */ 49 | public function setCreationOptions(array $options) 50 | { 51 | $this->creationOptions = $options; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Storage.php: -------------------------------------------------------------------------------- 1 | true, 25 | 'delete' => false, 26 | 'create' => false, 27 | 'top' => false, 28 | 'fetchPart' => true, 29 | 'flags' => false, 30 | ]; 31 | 32 | /** 33 | * current iteration position 34 | * @var int 35 | */ 36 | protected $iterationPos = 0; 37 | 38 | /** 39 | * maximum iteration position (= message count) 40 | * @var null|int 41 | */ 42 | protected $iterationMax = null; 43 | 44 | /** 45 | * used message class, change it in an extended class to extend the returned message class 46 | * @var string 47 | */ 48 | protected $messageClass = 'Zend\Mail\Storage\Message'; 49 | 50 | /** 51 | * Getter for has-properties. The standard has properties 52 | * are: hasFolder, hasUniqueid, hasDelete, hasCreate, hasTop 53 | * 54 | * The valid values for the has-properties are: 55 | * - true if a feature is supported 56 | * - false if a feature is not supported 57 | * - null is it's not yet known or it can't be know if a feature is supported 58 | * 59 | * @param string $var property name 60 | * @throws Exception\InvalidArgumentException 61 | * @return bool supported or not 62 | */ 63 | public function __get($var) 64 | { 65 | if (strpos($var, 'has') === 0) { 66 | $var = strtolower(substr($var, 3)); 67 | return isset($this->has[$var]) ? $this->has[$var] : null; 68 | } 69 | 70 | throw new Exception\InvalidArgumentException($var . ' not found'); 71 | } 72 | 73 | /** 74 | * Get a full list of features supported by the specific mail lib and the server 75 | * 76 | * @return array list of features as array(feature_name => true|false[|null]) 77 | */ 78 | public function getCapabilities() 79 | { 80 | return $this->has; 81 | } 82 | 83 | /** 84 | * Count messages messages in current box/folder 85 | * 86 | * @return int number of messages 87 | * @throws Exception\ExceptionInterface 88 | */ 89 | abstract public function countMessages(); 90 | 91 | /** 92 | * Get a list of messages with number and size 93 | * 94 | * @param int $id number of message 95 | * @return int|array size of given message of list with all messages as array(num => size) 96 | */ 97 | abstract public function getSize($id = 0); 98 | 99 | /** 100 | * Get a message with headers and body 101 | * 102 | * @param $id int number of message 103 | * @return Message 104 | */ 105 | abstract public function getMessage($id); 106 | 107 | /** 108 | * Get raw header of message or part 109 | * 110 | * @param int $id number of message 111 | * @param null|array|string $part path to part or null for message header 112 | * @param int $topLines include this many lines with header (after an empty line) 113 | * @return string raw header 114 | */ 115 | abstract public function getRawHeader($id, $part = null, $topLines = 0); 116 | 117 | /** 118 | * Get raw content of message or part 119 | * 120 | * @param int $id number of message 121 | * @param null|array|string $part path to part or null for message content 122 | * @return string raw content 123 | */ 124 | abstract public function getRawContent($id, $part = null); 125 | 126 | /** 127 | * Create instance with parameters 128 | * 129 | * @param array $params mail reader specific parameters 130 | * @throws Exception\ExceptionInterface 131 | */ 132 | abstract public function __construct($params); 133 | 134 | /** 135 | * Destructor calls close() and therefore closes the resource. 136 | */ 137 | public function __destruct() 138 | { 139 | $this->close(); 140 | } 141 | 142 | /** 143 | * Close resource for mail lib. If you need to control, when the resource 144 | * is closed. Otherwise the destructor would call this. 145 | */ 146 | abstract public function close(); 147 | 148 | /** 149 | * Keep the resource alive. 150 | */ 151 | abstract public function noop(); 152 | 153 | /** 154 | * delete a message from current box/folder 155 | * 156 | * @param $id 157 | */ 158 | abstract public function removeMessage($id); 159 | 160 | /** 161 | * get unique id for one or all messages 162 | * 163 | * if storage does not support unique ids it's the same as the message number 164 | * 165 | * @param int|null $id message number 166 | * @return array|string message number for given message or all messages as array 167 | * @throws Exception\ExceptionInterface 168 | */ 169 | abstract public function getUniqueId($id = null); 170 | 171 | /** 172 | * get a message number from a unique id 173 | * 174 | * I.e. if you have a webmailer that supports deleting messages you should use unique ids 175 | * as parameter and use this method to translate it to message number right before calling removeMessage() 176 | * 177 | * @param string $id unique id 178 | * @return int message number 179 | * @throws Exception\ExceptionInterface 180 | */ 181 | abstract public function getNumberByUniqueId($id); 182 | 183 | // interface implementations follows 184 | 185 | /** 186 | * Countable::count() 187 | * 188 | * @return int 189 | */ 190 | public function count() 191 | { 192 | return $this->countMessages(); 193 | } 194 | 195 | /** 196 | * ArrayAccess::offsetExists() 197 | * 198 | * @param int $id 199 | * @return bool 200 | */ 201 | public function offsetExists($id) 202 | { 203 | try { 204 | if ($this->getMessage($id)) { 205 | return true; 206 | } 207 | } catch (Exception\ExceptionInterface $e) { 208 | } 209 | 210 | return false; 211 | } 212 | 213 | /** 214 | * ArrayAccess::offsetGet() 215 | * 216 | * @param int $id 217 | * @return \Zend\Mail\Storage\Message message object 218 | */ 219 | public function offsetGet($id) 220 | { 221 | return $this->getMessage($id); 222 | } 223 | 224 | /** 225 | * ArrayAccess::offsetSet() 226 | * 227 | * @param mixed $id 228 | * @param mixed $value 229 | * @throws Exception\RuntimeException 230 | */ 231 | public function offsetSet($id, $value) 232 | { 233 | throw new Exception\RuntimeException('cannot write mail messages via array access'); 234 | } 235 | 236 | /** 237 | * ArrayAccess::offsetUnset() 238 | * 239 | * @param int $id 240 | * @return bool success 241 | */ 242 | public function offsetUnset($id) 243 | { 244 | return $this->removeMessage($id); 245 | } 246 | 247 | /** 248 | * Iterator::rewind() 249 | * 250 | * Rewind always gets the new count from the storage. Thus if you use 251 | * the interfaces and your scripts take long you should use reset() 252 | * from time to time. 253 | */ 254 | public function rewind() 255 | { 256 | $this->iterationMax = $this->countMessages(); 257 | $this->iterationPos = 1; 258 | } 259 | 260 | /** 261 | * Iterator::current() 262 | * 263 | * @return Message current message 264 | */ 265 | public function current() 266 | { 267 | return $this->getMessage($this->iterationPos); 268 | } 269 | 270 | /** 271 | * Iterator::key() 272 | * 273 | * @return int id of current position 274 | */ 275 | public function key() 276 | { 277 | return $this->iterationPos; 278 | } 279 | 280 | /** 281 | * Iterator::next() 282 | */ 283 | public function next() 284 | { 285 | ++$this->iterationPos; 286 | } 287 | 288 | /** 289 | * Iterator::valid() 290 | * 291 | * @return bool 292 | */ 293 | public function valid() 294 | { 295 | if ($this->iterationMax === null) { 296 | $this->iterationMax = $this->countMessages(); 297 | } 298 | return $this->iterationPos && $this->iterationPos <= $this->iterationMax; 299 | } 300 | 301 | /** 302 | * SeekableIterator::seek() 303 | * 304 | * @param int $pos 305 | * @throws Exception\OutOfBoundsException 306 | */ 307 | public function seek($pos) 308 | { 309 | if ($this->iterationMax === null) { 310 | $this->iterationMax = $this->countMessages(); 311 | } 312 | 313 | if ($pos > $this->iterationMax) { 314 | throw new Exception\OutOfBoundsException('this position does not exist'); 315 | } 316 | $this->iterationPos = $pos; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/Storage/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | \Zend\Mail\Storage\Folder folder) 16 | * @var array 17 | */ 18 | protected $folders; 19 | 20 | /** 21 | * local name (name of folder in parent folder) 22 | * @var string 23 | */ 24 | protected $localName; 25 | 26 | /** 27 | * global name (absolute name of folder) 28 | * @var string 29 | */ 30 | protected $globalName; 31 | 32 | /** 33 | * folder is selectable if folder is able to hold messages, otherwise it is a parent folder 34 | * @var bool 35 | */ 36 | protected $selectable = true; 37 | 38 | /** 39 | * create a new mail folder instance 40 | * 41 | * @param string $localName name of folder in current subdirectory 42 | * @param string $globalName absolute name of folder 43 | * @param bool $selectable if true folder holds messages, if false it's 44 | * just a parent for subfolders (Default: true) 45 | * @param array $folders init with given instances of Folder as subfolders 46 | */ 47 | public function __construct($localName, $globalName = '', $selectable = true, array $folders = []) 48 | { 49 | $this->localName = $localName; 50 | $this->globalName = $globalName ? $globalName : $localName; 51 | $this->selectable = $selectable; 52 | $this->folders = $folders; 53 | } 54 | 55 | /** 56 | * implements RecursiveIterator::hasChildren() 57 | * 58 | * @return bool current element has children 59 | */ 60 | public function hasChildren() 61 | { 62 | $current = $this->current(); 63 | return $current && $current instanceof Folder && ! $current->isLeaf(); 64 | } 65 | 66 | /** 67 | * implements RecursiveIterator::getChildren() 68 | * 69 | * @return \Zend\Mail\Storage\Folder same as self::current() 70 | */ 71 | public function getChildren() 72 | { 73 | return $this->current(); 74 | } 75 | 76 | /** 77 | * implements Iterator::valid() 78 | * 79 | * @return bool check if there's a current element 80 | */ 81 | public function valid() 82 | { 83 | return key($this->folders) !== null; 84 | } 85 | 86 | /** 87 | * implements Iterator::next() 88 | */ 89 | public function next() 90 | { 91 | next($this->folders); 92 | } 93 | 94 | /** 95 | * implements Iterator::key() 96 | * 97 | * @return string key/local name of current element 98 | */ 99 | public function key() 100 | { 101 | return key($this->folders); 102 | } 103 | 104 | /** 105 | * implements Iterator::current() 106 | * 107 | * @return \Zend\Mail\Storage\Folder current folder 108 | */ 109 | public function current() 110 | { 111 | return current($this->folders); 112 | } 113 | 114 | /** 115 | * implements Iterator::rewind() 116 | */ 117 | public function rewind() 118 | { 119 | reset($this->folders); 120 | } 121 | 122 | /** 123 | * get subfolder named $name 124 | * 125 | * @param string $name wanted subfolder 126 | * @throws Exception\InvalidArgumentException 127 | * @return \Zend\Mail\Storage\Folder folder named $folder 128 | */ 129 | public function __get($name) 130 | { 131 | if (! isset($this->folders[$name])) { 132 | throw new Exception\InvalidArgumentException("no subfolder named $name"); 133 | } 134 | 135 | return $this->folders[$name]; 136 | } 137 | 138 | /** 139 | * add or replace subfolder named $name 140 | * 141 | * @param string $name local name of subfolder 142 | * @param \Zend\Mail\Storage\Folder $folder instance for new subfolder 143 | */ 144 | public function __set($name, Folder $folder) 145 | { 146 | $this->folders[$name] = $folder; 147 | } 148 | 149 | /** 150 | * remove subfolder named $name 151 | * 152 | * @param string $name local name of subfolder 153 | */ 154 | public function __unset($name) 155 | { 156 | unset($this->folders[$name]); 157 | } 158 | 159 | /** 160 | * magic method for easy output of global name 161 | * 162 | * @return string global name of folder 163 | */ 164 | public function __toString() 165 | { 166 | return (string) $this->getGlobalName(); 167 | } 168 | 169 | /** 170 | * get local name 171 | * 172 | * @return string local name 173 | */ 174 | public function getLocalName() 175 | { 176 | return $this->localName; 177 | } 178 | 179 | /** 180 | * get global name 181 | * 182 | * @return string global name 183 | */ 184 | public function getGlobalName() 185 | { 186 | return $this->globalName; 187 | } 188 | 189 | /** 190 | * is this folder selectable? 191 | * 192 | * @return bool selectable 193 | */ 194 | public function isSelectable() 195 | { 196 | return $this->selectable; 197 | } 198 | 199 | /** 200 | * check if folder has no subfolder 201 | * 202 | * @return bool true if no subfolders 203 | */ 204 | public function isLeaf() 205 | { 206 | return empty($this->folders); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Storage/Folder/FolderInterface.php: -------------------------------------------------------------------------------- 1 | dirname) || ! is_dir($params->dirname)) { 59 | throw new Exception\InvalidArgumentException('no valid dirname given in params'); 60 | } 61 | 62 | $this->rootdir = rtrim($params->dirname, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; 63 | 64 | $this->delim = isset($params->delim) ? $params->delim : '.'; 65 | 66 | $this->buildFolderTree(); 67 | $this->selectFolder(! empty($params->folder) ? $params->folder : 'INBOX'); 68 | $this->has['top'] = true; 69 | $this->has['flags'] = true; 70 | } 71 | 72 | /** 73 | * find all subfolders and mbox files for folder structure 74 | * 75 | * Result is save in Storage\Folder instances with the root in $this->rootFolder. 76 | * $parentFolder and $parentGlobalName are only used internally for recursion. 77 | * 78 | * @throws Exception\RuntimeException 79 | */ 80 | protected function buildFolderTree() 81 | { 82 | $this->rootFolder = new Storage\Folder('/', '/', false); 83 | $this->rootFolder->INBOX = new Storage\Folder('INBOX', 'INBOX', true); 84 | 85 | ErrorHandler::start(E_WARNING); 86 | $dh = opendir($this->rootdir); 87 | $error = ErrorHandler::stop(); 88 | if (! $dh) { 89 | throw new Exception\RuntimeException("can't read folders in maildir", 0, $error); 90 | } 91 | $dirs = []; 92 | 93 | while (($entry = readdir($dh)) !== false) { 94 | // maildir++ defines folders must start with . 95 | if ($entry[0] != '.' || $entry == '.' || $entry == '..') { 96 | continue; 97 | } 98 | 99 | if ($this->_isMaildir($this->rootdir . $entry)) { 100 | $dirs[] = $entry; 101 | } 102 | } 103 | closedir($dh); 104 | 105 | sort($dirs); 106 | $stack = [null]; 107 | $folderStack = [null]; 108 | $parentFolder = $this->rootFolder; 109 | $parent = '.'; 110 | 111 | foreach ($dirs as $dir) { 112 | do { 113 | if (strpos($dir, $parent) === 0) { 114 | $local = substr($dir, strlen($parent)); 115 | if (strpos($local, $this->delim) !== false) { 116 | throw new Exception\RuntimeException('error while reading maildir'); 117 | } 118 | array_push($stack, $parent); 119 | $parent = $dir . $this->delim; 120 | $folder = new Storage\Folder($local, substr($dir, 1), true); 121 | $parentFolder->$local = $folder; 122 | array_push($folderStack, $parentFolder); 123 | $parentFolder = $folder; 124 | break; 125 | } elseif ($stack) { 126 | $parent = array_pop($stack); 127 | $parentFolder = array_pop($folderStack); 128 | } 129 | } while ($stack); 130 | if (! $stack) { 131 | throw new Exception\RuntimeException('error while reading maildir'); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * get root folder or given folder 138 | * 139 | * @param string $rootFolder get folder structure for given folder, else root 140 | * @throws \Zend\Mail\Storage\Exception\InvalidArgumentException 141 | * @return \Zend\Mail\Storage\Folder root or wanted folder 142 | */ 143 | public function getFolders($rootFolder = null) 144 | { 145 | if (! $rootFolder || $rootFolder == 'INBOX') { 146 | return $this->rootFolder; 147 | } 148 | 149 | // rootdir is same as INBOX in maildir 150 | if (strpos($rootFolder, 'INBOX' . $this->delim) === 0) { 151 | $rootFolder = substr($rootFolder, 6); 152 | } 153 | $currentFolder = $this->rootFolder; 154 | $subname = trim($rootFolder, $this->delim); 155 | 156 | while ($currentFolder) { 157 | ErrorHandler::start(E_NOTICE); 158 | list($entry, $subname) = explode($this->delim, $subname, 2); 159 | ErrorHandler::stop(); 160 | $currentFolder = $currentFolder->$entry; 161 | if (! $subname) { 162 | break; 163 | } 164 | } 165 | 166 | if ($currentFolder->getGlobalName() != rtrim($rootFolder, $this->delim)) { 167 | throw new Exception\InvalidArgumentException("folder $rootFolder not found"); 168 | } 169 | return $currentFolder; 170 | } 171 | 172 | /** 173 | * select given folder 174 | * 175 | * folder must be selectable! 176 | * 177 | * @param Storage\Folder|string $globalName global name of folder or 178 | * instance for subfolder 179 | * @throws Exception\RuntimeException 180 | */ 181 | public function selectFolder($globalName) 182 | { 183 | $this->currentFolder = (string) $globalName; 184 | 185 | // getting folder from folder tree for validation 186 | $folder = $this->getFolders($this->currentFolder); 187 | 188 | try { 189 | $this->_openMaildir($this->rootdir . '.' . $folder->getGlobalName()); 190 | } catch (Exception\ExceptionInterface $e) { 191 | // check what went wrong 192 | if (! $folder->isSelectable()) { 193 | throw new Exception\RuntimeException("{$this->currentFolder} is not selectable", 0, $e); 194 | } 195 | // seems like file has vanished; rebuilding folder tree - but it's still an exception 196 | $this->buildFolderTree(); 197 | throw new Exception\RuntimeException( 198 | 'seems like the maildir has vanished; I have rebuilt the folder tree; ' 199 | . 'search for another folder and try again', 200 | 0, 201 | $e 202 | ); 203 | } 204 | } 205 | 206 | /** 207 | * get Storage\Folder instance for current folder 208 | * 209 | * @return Storage\Folder instance of current folder 210 | */ 211 | public function getCurrentFolder() 212 | { 213 | return $this->currentFolder; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Storage/Folder/Mbox.php: -------------------------------------------------------------------------------- 1 | filename)) { 55 | throw new Exception\InvalidArgumentException('use \Zend\Mail\Storage\Mbox for a single file'); 56 | } 57 | 58 | if (! isset($params->dirname) || ! is_dir($params->dirname)) { 59 | throw new Exception\InvalidArgumentException('no valid dirname given in params'); 60 | } 61 | 62 | $this->rootdir = rtrim($params->dirname, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; 63 | 64 | $this->buildFolderTree($this->rootdir); 65 | $this->selectFolder(! empty($params->folder) ? $params->folder : 'INBOX'); 66 | $this->has['top'] = true; 67 | $this->has['uniqueid'] = false; 68 | } 69 | 70 | /** 71 | * find all subfolders and mbox files for folder structure 72 | * 73 | * Result is save in Storage\Folder instances with the root in $this->rootFolder. 74 | * $parentFolder and $parentGlobalName are only used internally for recursion. 75 | * 76 | * @param string $currentDir call with root dir, also used for recursion. 77 | * @param Storage\Folder|null $parentFolder used for recursion 78 | * @param string $parentGlobalName used for recursion 79 | * @throws Exception\InvalidArgumentException 80 | */ 81 | protected function buildFolderTree($currentDir, $parentFolder = null, $parentGlobalName = '') 82 | { 83 | if (! $parentFolder) { 84 | $this->rootFolder = new Storage\Folder('/', '/', false); 85 | $parentFolder = $this->rootFolder; 86 | } 87 | 88 | ErrorHandler::start(E_WARNING); 89 | $dh = opendir($currentDir); 90 | ErrorHandler::stop(); 91 | if (! $dh) { 92 | throw new Exception\InvalidArgumentException("can't read dir $currentDir"); 93 | } 94 | while (($entry = readdir($dh)) !== false) { 95 | // ignore hidden files for mbox 96 | if ($entry[0] == '.') { 97 | continue; 98 | } 99 | $absoluteEntry = $currentDir . $entry; 100 | $globalName = $parentGlobalName . DIRECTORY_SEPARATOR . $entry; 101 | if (is_file($absoluteEntry) && $this->isMboxFile($absoluteEntry)) { 102 | $parentFolder->$entry = new Storage\Folder($entry, $globalName); 103 | continue; 104 | } 105 | if (! is_dir($absoluteEntry) /* || $entry == '.' || $entry == '..' */) { 106 | continue; 107 | } 108 | $folder = new Storage\Folder($entry, $globalName, false); 109 | $parentFolder->$entry = $folder; 110 | $this->buildFolderTree($absoluteEntry . DIRECTORY_SEPARATOR, $folder, $globalName); 111 | } 112 | 113 | closedir($dh); 114 | } 115 | 116 | /** 117 | * get root folder or given folder 118 | * 119 | * @param string $rootFolder get folder structure for given folder, else root 120 | * @return Storage\Folder root or wanted folder 121 | * @throws Exception\InvalidArgumentException 122 | */ 123 | public function getFolders($rootFolder = null) 124 | { 125 | if (! $rootFolder) { 126 | return $this->rootFolder; 127 | } 128 | 129 | $currentFolder = $this->rootFolder; 130 | $subname = trim($rootFolder, DIRECTORY_SEPARATOR); 131 | while ($currentFolder) { 132 | ErrorHandler::start(E_NOTICE); 133 | list($entry, $subname) = explode(DIRECTORY_SEPARATOR, $subname, 2); 134 | ErrorHandler::stop(); 135 | $currentFolder = $currentFolder->$entry; 136 | if (! $subname) { 137 | break; 138 | } 139 | } 140 | 141 | if ($currentFolder->getGlobalName() != DIRECTORY_SEPARATOR . trim($rootFolder, DIRECTORY_SEPARATOR)) { 142 | throw new Exception\InvalidArgumentException("folder $rootFolder not found"); 143 | } 144 | return $currentFolder; 145 | } 146 | 147 | /** 148 | * select given folder 149 | * 150 | * folder must be selectable! 151 | * 152 | * @param Storage\Folder|string $globalName global name of folder or 153 | * instance for subfolder 154 | * @throws Exception\RuntimeException 155 | */ 156 | public function selectFolder($globalName) 157 | { 158 | $this->currentFolder = (string) $globalName; 159 | 160 | // getting folder from folder tree for validation 161 | $folder = $this->getFolders($this->currentFolder); 162 | 163 | try { 164 | $this->openMboxFile($this->rootdir . $folder->getGlobalName()); 165 | } catch (Exception\ExceptionInterface $e) { 166 | // check what went wrong 167 | if (! $folder->isSelectable()) { 168 | throw new Exception\RuntimeException("{$this->currentFolder} is not selectable", 0, $e); 169 | } 170 | // seems like file has vanished; rebuilding folder tree - but it's still an exception 171 | $this->buildFolderTree($this->rootdir); 172 | throw new Exception\RuntimeException( 173 | 'seems like the mbox file has vanished; I have rebuilt the folder tree; ' 174 | . 'search for another folder and try again', 175 | 0, 176 | $e 177 | ); 178 | } 179 | } 180 | 181 | /** 182 | * get Storage\Folder instance for current folder 183 | * 184 | * @return Storage\Folder instance of current folder 185 | * @throws Exception\ExceptionInterface 186 | */ 187 | public function getCurrentFolder() 188 | { 189 | return $this->currentFolder; 190 | } 191 | 192 | /** 193 | * magic method for serialize() 194 | * 195 | * with this method you can cache the mbox class 196 | * 197 | * @return array name of variables 198 | */ 199 | public function __sleep() 200 | { 201 | return array_merge(parent::__sleep(), ['currentFolder', 'rootFolder', 'rootdir']); 202 | } 203 | 204 | /** 205 | * magic method for unserialize(), with this method you can cache the mbox class 206 | */ 207 | public function __wakeup() 208 | { 209 | // if cache is stall selectFolder() rebuilds the tree on error 210 | parent::__wakeup(); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Storage/Message.php: -------------------------------------------------------------------------------- 1 | flags = array_combine($params['flags'], $params['flags']); 50 | } 51 | 52 | parent::__construct($params); 53 | } 54 | 55 | /** 56 | * return toplines as found after headers 57 | * 58 | * @return string toplines 59 | */ 60 | public function getTopLines() 61 | { 62 | return $this->topLines; 63 | } 64 | 65 | /** 66 | * check if flag is set 67 | * 68 | * @param mixed $flag a flag name, use constants defined in \Zend\Mail\Storage 69 | * @return bool true if set, otherwise false 70 | */ 71 | public function hasFlag($flag) 72 | { 73 | return isset($this->flags[$flag]); 74 | } 75 | 76 | /** 77 | * get all set flags 78 | * 79 | * @return array array with flags, key and value are the same for easy lookup 80 | */ 81 | public function getFlags() 82 | { 83 | return $this->flags; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Storage/Message/File.php: -------------------------------------------------------------------------------- 1 | flags = array_combine($params['flags'], $params['flags']); 34 | } 35 | 36 | parent::__construct($params); 37 | } 38 | 39 | /** 40 | * return toplines as found after headers 41 | * 42 | * @return string toplines 43 | */ 44 | public function getTopLines() 45 | { 46 | return $this->topLines; 47 | } 48 | 49 | /** 50 | * check if flag is set 51 | * 52 | * @param mixed $flag a flag name, use constants defined in \Zend\Mail\Storage 53 | * @return bool true if set, otherwise false 54 | */ 55 | public function hasFlag($flag) 56 | { 57 | return isset($this->flags[$flag]); 58 | } 59 | 60 | /** 61 | * get all set flags 62 | * 63 | * @return array array with flags, key and value are the same for easy lookup 64 | */ 65 | public function getFlags() 66 | { 67 | return $this->flags; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Storage/Message/MessageInterface.php: -------------------------------------------------------------------------------- 1 | fh = fopen($params['file'], 'r'); 40 | } else { 41 | $this->fh = $params['file']; 42 | } 43 | if (! $this->fh) { 44 | throw new Exception\RuntimeException('could not open file'); 45 | } 46 | if (isset($params['startPos'])) { 47 | fseek($this->fh, $params['startPos']); 48 | } 49 | $header = ''; 50 | $endPos = isset($params['endPos']) ? $params['endPos'] : null; 51 | while (($endPos === null || ftell($this->fh) < $endPos) && trim($line = fgets($this->fh))) { 52 | $header .= $line; 53 | } 54 | 55 | if (isset($params['EOL'])) { 56 | $this->headers = Headers::fromString($header, $params['EOL']); 57 | } else { 58 | $this->headers = Headers::fromString($header); 59 | } 60 | 61 | $this->contentPos[0] = ftell($this->fh); 62 | if ($endPos !== null) { 63 | $this->contentPos[1] = $endPos; 64 | } else { 65 | fseek($this->fh, 0, SEEK_END); 66 | $this->contentPos[1] = ftell($this->fh); 67 | } 68 | if (! $this->isMultipart()) { 69 | return; 70 | } 71 | 72 | $boundary = $this->getHeaderField('content-type', 'boundary'); 73 | if (! $boundary) { 74 | throw new Exception\RuntimeException('no boundary found in content type to split message'); 75 | } 76 | 77 | $part = []; 78 | $pos = $this->contentPos[0]; 79 | fseek($this->fh, $pos); 80 | while (! feof($this->fh) && ($endPos === null || $pos < $endPos)) { 81 | $line = fgets($this->fh); 82 | if ($line === false) { 83 | if (feof($this->fh)) { 84 | break; 85 | } 86 | throw new Exception\RuntimeException('error reading file'); 87 | } 88 | 89 | $lastPos = $pos; 90 | $pos = ftell($this->fh); 91 | $line = trim($line); 92 | 93 | if ($line == '--' . $boundary) { 94 | if ($part) { 95 | // not first part 96 | $part[1] = $lastPos; 97 | $this->partPos[] = $part; 98 | } 99 | $part = [$pos]; 100 | } elseif ($line == '--' . $boundary . '--') { 101 | $part[1] = $lastPos; 102 | $this->partPos[] = $part; 103 | break; 104 | } 105 | } 106 | $this->countParts = count($this->partPos); 107 | } 108 | 109 | /** 110 | * Body of part 111 | * 112 | * If part is multipart the raw content of this part with all sub parts is returned 113 | * 114 | * @param resource $stream Optional 115 | * @return string body 116 | */ 117 | public function getContent($stream = null) 118 | { 119 | fseek($this->fh, $this->contentPos[0]); 120 | if ($stream !== null) { 121 | return stream_copy_to_stream($this->fh, $stream, $this->contentPos[1] - $this->contentPos[0]); 122 | } 123 | $length = $this->contentPos[1] - $this->contentPos[0]; 124 | return $length < 1 ? '' : fread($this->fh, $length); 125 | } 126 | 127 | /** 128 | * Return size of part 129 | * 130 | * Quite simple implemented currently (not decoding). Handle with care. 131 | * 132 | * @return int size 133 | */ 134 | public function getSize() 135 | { 136 | return $this->contentPos[1] - $this->contentPos[0]; 137 | } 138 | 139 | /** 140 | * Get part of multipart message 141 | * 142 | * @param int $num number of part starting with 1 for first part 143 | * @throws Exception\RuntimeException 144 | * @return Part wanted part 145 | */ 146 | public function getPart($num) 147 | { 148 | --$num; 149 | if (! isset($this->partPos[$num])) { 150 | throw new Exception\RuntimeException('part not found'); 151 | } 152 | 153 | return new static(['file' => $this->fh, 'startPos' => $this->partPos[$num][0], 154 | 'endPos' => $this->partPos[$num][1]]); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Storage/Part/PartInterface.php: -------------------------------------------------------------------------------- 1 | firstPart, partname => value] 100 | * @throws Exception\ExceptionInterface 101 | */ 102 | public function getHeaderField($name, $wantedPart = '0', $firstName = '0'); 103 | 104 | /** 105 | * Getter for mail headers - name is matched in lowercase 106 | * 107 | * This getter is short for PartInterface::getHeader($name, 'string') 108 | * 109 | * @see PartInterface::getHeader() 110 | * @param string $name header name 111 | * @return string value of header 112 | * @throws Exception\ExceptionInterface 113 | */ 114 | public function __get($name); 115 | 116 | /** 117 | * magic method to get content of part 118 | * 119 | * @return string content 120 | */ 121 | public function __toString(); 122 | } 123 | -------------------------------------------------------------------------------- /src/Storage/Pop3.php: -------------------------------------------------------------------------------- 1 | protocol->status($count, $octets); 34 | return (int) $count; 35 | } 36 | 37 | /** 38 | * get a list of messages with number and size 39 | * 40 | * @param int $id number of message 41 | * @return int|array size of given message of list with all messages as array(num => size) 42 | * @throws \Zend\Mail\Protocol\Exception\ExceptionInterface 43 | */ 44 | public function getSize($id = 0) 45 | { 46 | $id = $id ? $id : null; 47 | return $this->protocol->getList($id); 48 | } 49 | 50 | /** 51 | * Fetch a message 52 | * 53 | * @param int $id number of message 54 | * @return \Zend\Mail\Storage\Message 55 | * @throws \Zend\Mail\Protocol\Exception\ExceptionInterface 56 | */ 57 | public function getMessage($id) 58 | { 59 | $bodyLines = 0; 60 | $message = $this->protocol->top($id, $bodyLines, true); 61 | 62 | return new $this->messageClass(['handler' => $this, 'id' => $id, 'headers' => $message, 63 | 'noToplines' => $bodyLines < 1]); 64 | } 65 | 66 | /* 67 | * Get raw header of message or part 68 | * 69 | * @param int $id number of message 70 | * @param null|array|string $part path to part or null for message header 71 | * @param int $topLines include this many lines with header (after an empty line) 72 | * @return string raw header 73 | * @throws \Zend\Mail\Protocol\Exception\ExceptionInterface 74 | * @throws \Zend\Mail\Storage\Exception\ExceptionInterface 75 | */ 76 | public function getRawHeader($id, $part = null, $topLines = 0) 77 | { 78 | if ($part !== null) { 79 | // TODO: implement 80 | throw new Exception\RuntimeException('not implemented'); 81 | } 82 | 83 | return $this->protocol->top($id, 0, true); 84 | } 85 | 86 | /* 87 | * Get raw content of message or part 88 | * 89 | * @param int $id number of message 90 | * @param null|array|string $part path to part or null for message content 91 | * @return string raw content 92 | * @throws \Zend\Mail\Protocol\Exception\ExceptionInterface 93 | * @throws \Zend\Mail\Storage\Exception\ExceptionInterface 94 | */ 95 | public function getRawContent($id, $part = null) 96 | { 97 | if ($part !== null) { 98 | // TODO: implement 99 | throw new Exception\RuntimeException('not implemented'); 100 | } 101 | 102 | $content = $this->protocol->retrieve($id); 103 | // TODO: find a way to avoid decoding the headers 104 | $headers = null; // "Declare" variable since it's passed by reference 105 | $body = null; // "Declare" variable before first usage. 106 | Mime\Decode::splitMessage($content, $headers, $body); 107 | return $body; 108 | } 109 | 110 | /** 111 | * create instance with parameters 112 | * Supported parameters are 113 | * - host hostname or ip address of POP3 server 114 | * - user username 115 | * - password password for user 'username' [optional, default = ''] 116 | * - port port for POP3 server [optional, default = 110] 117 | * - ssl 'SSL' or 'TLS' for secure sockets 118 | * 119 | * @param $params array mail reader specific parameters 120 | * @throws \Zend\Mail\Storage\Exception\InvalidArgumentException 121 | * @throws \Zend\Mail\Protocol\Exception\RuntimeException 122 | */ 123 | public function __construct($params) 124 | { 125 | if (is_array($params)) { 126 | $params = (object) $params; 127 | } 128 | 129 | $this->has['fetchPart'] = false; 130 | $this->has['top'] = null; 131 | $this->has['uniqueid'] = null; 132 | 133 | if ($params instanceof Protocol\Pop3) { 134 | $this->protocol = $params; 135 | return; 136 | } 137 | 138 | if (! isset($params->user)) { 139 | throw new Exception\InvalidArgumentException('need at least user in params'); 140 | } 141 | 142 | $host = isset($params->host) ? $params->host : 'localhost'; 143 | $password = isset($params->password) ? $params->password : ''; 144 | $port = isset($params->port) ? $params->port : null; 145 | $ssl = isset($params->ssl) ? $params->ssl : false; 146 | 147 | $this->protocol = new Protocol\Pop3(); 148 | $this->protocol->connect($host, $port, $ssl); 149 | $this->protocol->login($params->user, $password); 150 | } 151 | 152 | /** 153 | * Close resource for mail lib. If you need to control, when the resource 154 | * is closed. Otherwise the destructor would call this. 155 | */ 156 | public function close() 157 | { 158 | $this->protocol->logout(); 159 | } 160 | 161 | /** 162 | * Keep the server busy. 163 | * 164 | * @throws \Zend\Mail\Protocol\Exception\RuntimeException 165 | */ 166 | public function noop() 167 | { 168 | $this->protocol->noop(); 169 | } 170 | 171 | /** 172 | * Remove a message from server. If you're doing that from a web environment 173 | * you should be careful and use a uniqueid as parameter if possible to 174 | * identify the message. 175 | * 176 | * @param int $id number of message 177 | * @throws \Zend\Mail\Protocol\Exception\RuntimeException 178 | */ 179 | public function removeMessage($id) 180 | { 181 | $this->protocol->delete($id); 182 | } 183 | 184 | /** 185 | * get unique id for one or all messages 186 | * 187 | * if storage does not support unique ids it's the same as the message number 188 | * 189 | * @param int|null $id message number 190 | * @return array|string message number for given message or all messages as array 191 | * @throws \Zend\Mail\Storage\Exception\ExceptionInterface 192 | */ 193 | public function getUniqueId($id = null) 194 | { 195 | if (! $this->hasUniqueid) { 196 | if ($id) { 197 | return $id; 198 | } 199 | $count = $this->countMessages(); 200 | if ($count < 1) { 201 | return []; 202 | } 203 | $range = range(1, $count); 204 | return array_combine($range, $range); 205 | } 206 | 207 | return $this->protocol->uniqueid($id); 208 | } 209 | 210 | /** 211 | * get a message number from a unique id 212 | * 213 | * I.e. if you have a webmailer that supports deleting messages you should use unique ids 214 | * as parameter and use this method to translate it to message number right before calling removeMessage() 215 | * 216 | * @param string $id unique id 217 | * @throws Exception\InvalidArgumentException 218 | * @return int message number 219 | */ 220 | public function getNumberByUniqueId($id) 221 | { 222 | if (! $this->hasUniqueid) { 223 | return $id; 224 | } 225 | 226 | $ids = $this->getUniqueId(); 227 | foreach ($ids as $k => $v) { 228 | if ($v == $id) { 229 | return $k; 230 | } 231 | } 232 | 233 | throw new Exception\InvalidArgumentException('unique id not found'); 234 | } 235 | 236 | /** 237 | * Special handling for hasTop and hasUniqueid. The headers of the first message is 238 | * retrieved if Top wasn't needed/tried yet. 239 | * 240 | * @see AbstractStorage::__get() 241 | * @param string $var 242 | * @return string 243 | */ 244 | public function __get($var) 245 | { 246 | $result = parent::__get($var); 247 | if ($result !== null) { 248 | return $result; 249 | } 250 | 251 | if (strtolower($var) == 'hastop') { 252 | if ($this->protocol->hasTop === null) { 253 | // need to make a real call, because not all server are honest in their capas 254 | try { 255 | $this->protocol->top(1, 0, false); 256 | } catch (MailException\ExceptionInterface $e) { 257 | // ignoring error 258 | } 259 | } 260 | $this->has['top'] = $this->protocol->hasTop; 261 | return $this->protocol->hasTop; 262 | } 263 | 264 | if (strtolower($var) == 'hasuniqueid') { 265 | $id = null; 266 | try { 267 | $id = $this->protocol->uniqueid(1); 268 | } catch (MailException\ExceptionInterface $e) { 269 | // ignoring error 270 | } 271 | $this->has['uniqueid'] = (bool) $id; 272 | return $this->has['uniqueid']; 273 | } 274 | 275 | return $result; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/Storage/Writable/WritableInterface.php: -------------------------------------------------------------------------------- 1 | from; 32 | } 33 | 34 | /** 35 | * Set MAIL FROM 36 | * 37 | * @param string $from 38 | */ 39 | public function setFrom($from) 40 | { 41 | $this->from = (string) $from; 42 | } 43 | 44 | /** 45 | * Get RCPT TO 46 | * 47 | * @return string|null 48 | */ 49 | public function getTo() 50 | { 51 | return $this->to; 52 | } 53 | 54 | /** 55 | * Set RCPT TO 56 | * 57 | * @param string $to 58 | */ 59 | public function setTo($to) 60 | { 61 | $this->to = $to; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Transport/Exception/DomainException.php: -------------------------------------------------------------------------------- 1 | 'Zend\Mail\Transport\File', 20 | 'inmemory' => 'Zend\Mail\Transport\InMemory', 21 | 'memory' => 'Zend\Mail\Transport\InMemory', 22 | 'null' => 'Zend\Mail\Transport\InMemory', 23 | 'sendmail' => 'Zend\Mail\Transport\Sendmail', 24 | 'smtp' => 'Zend\Mail\Transport\Smtp', 25 | ]; 26 | 27 | /** 28 | * @param array $spec 29 | * @return TransportInterface 30 | * @throws Exception\InvalidArgumentException 31 | * @throws Exception\DomainException 32 | */ 33 | public static function create($spec = []) 34 | { 35 | if ($spec instanceof Traversable) { 36 | $spec = ArrayUtils::iteratorToArray($spec); 37 | } 38 | 39 | if (! is_array($spec)) { 40 | throw new Exception\InvalidArgumentException(sprintf( 41 | '%s expects an array or Traversable argument; received "%s"', 42 | __METHOD__, 43 | (is_object($spec) ? get_class($spec) : gettype($spec)) 44 | )); 45 | } 46 | 47 | $type = isset($spec['type']) ? $spec['type'] : 'sendmail'; 48 | 49 | $normalizedType = strtolower($type); 50 | 51 | if (isset(static::$classMap[$normalizedType])) { 52 | $type = static::$classMap[$normalizedType]; 53 | } 54 | 55 | if (! class_exists($type)) { 56 | throw new Exception\DomainException(sprintf( 57 | '%s expects the "type" attribute to resolve to an existing class; received "%s"', 58 | __METHOD__, 59 | $type 60 | )); 61 | } 62 | 63 | $transport = new $type; 64 | 65 | if (! $transport instanceof TransportInterface) { 66 | throw new Exception\DomainException(sprintf( 67 | '%s expects the "type" attribute to resolve to a valid' 68 | . ' Zend\Mail\Transport\TransportInterface instance; received "%s"', 69 | __METHOD__, 70 | $type 71 | )); 72 | } 73 | 74 | if ($transport instanceof Smtp && isset($spec['options'])) { 75 | $transport->setOptions(new SmtpOptions($spec['options'])); 76 | } 77 | 78 | if ($transport instanceof File && isset($spec['options'])) { 79 | $transport->setOptions(new FileOptions($spec['options'])); 80 | } 81 | 82 | return $transport; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Transport/File.php: -------------------------------------------------------------------------------- 1 | setOptions($options); 42 | } 43 | 44 | /** 45 | * @return FileOptions 46 | */ 47 | public function getOptions() 48 | { 49 | return $this->options; 50 | } 51 | 52 | /** 53 | * Sets options 54 | * 55 | * @param FileOptions $options 56 | */ 57 | public function setOptions(FileOptions $options) 58 | { 59 | $this->options = $options; 60 | } 61 | 62 | /** 63 | * Saves e-mail message to a file 64 | * 65 | * @param Message $message 66 | * @throws Exception\RuntimeException on not writable target directory or 67 | * on file_put_contents() failure 68 | */ 69 | public function send(Message $message) 70 | { 71 | $options = $this->options; 72 | $filename = call_user_func($options->getCallback(), $this); 73 | $file = $options->getPath() . DIRECTORY_SEPARATOR . $filename; 74 | $email = $message->toString(); 75 | 76 | if (false === file_put_contents($file, $email)) { 77 | throw new Exception\RuntimeException(sprintf( 78 | 'Unable to write mail to file (directory "%s")', 79 | $options->getPath() 80 | )); 81 | } 82 | 83 | $this->lastFile = $file; 84 | } 85 | 86 | /** 87 | * Get the name of the last file written to 88 | * 89 | * @return string 90 | */ 91 | public function getLastFile() 92 | { 93 | return $this->lastFile; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Transport/FileOptions.php: -------------------------------------------------------------------------------- 1 | path = $path; 42 | return $this; 43 | } 44 | 45 | /** 46 | * Get path 47 | * 48 | * If none is set, uses value from sys_get_temp_dir() 49 | * 50 | * @return string 51 | */ 52 | public function getPath() 53 | { 54 | if (null === $this->path) { 55 | $this->setPath(sys_get_temp_dir()); 56 | } 57 | return $this->path; 58 | } 59 | 60 | /** 61 | * Set callback used to generate a file name 62 | * 63 | * @param callable $callback 64 | * @throws \Zend\Mail\Exception\InvalidArgumentException 65 | * @return FileOptions 66 | */ 67 | public function setCallback($callback) 68 | { 69 | if (! is_callable($callback)) { 70 | throw new Exception\InvalidArgumentException(sprintf( 71 | '%s expects a valid callback; received "%s"', 72 | __METHOD__, 73 | (is_object($callback) ? get_class($callback) : gettype($callback)) 74 | )); 75 | } 76 | $this->callback = $callback; 77 | return $this; 78 | } 79 | 80 | /** 81 | * Get callback used to generate a file name 82 | * 83 | * @return callable 84 | */ 85 | public function getCallback() 86 | { 87 | if (null === $this->callback) { 88 | $this->setCallback(function () { 89 | return 'ZendMail_' . time() . '_' . mt_rand() . '.eml'; 90 | }); 91 | } 92 | return $this->callback; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Transport/InMemory.php: -------------------------------------------------------------------------------- 1 | lastMessage = $message; 34 | } 35 | 36 | /** 37 | * Get the last message sent. 38 | * 39 | * @return Message 40 | */ 41 | public function getLastMessage() 42 | { 43 | return $this->lastMessage; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Transport/Null.php: -------------------------------------------------------------------------------- 1 | name; 58 | } 59 | 60 | /** 61 | * Set the local client hostname or IP 62 | * 63 | * @todo hostname/IP validation 64 | * @param string $name 65 | * @throws \Zend\Mail\Exception\InvalidArgumentException 66 | * @return SmtpOptions 67 | */ 68 | public function setName($name) 69 | { 70 | if (! is_string($name) && $name !== null) { 71 | throw new Exception\InvalidArgumentException(sprintf( 72 | 'Name must be a string or null; argument of type "%s" provided', 73 | (is_object($name) ? get_class($name) : gettype($name)) 74 | )); 75 | } 76 | $this->name = $name; 77 | return $this; 78 | } 79 | 80 | /** 81 | * Get connection class 82 | * 83 | * This should be either the class Zend\Mail\Protocol\Smtp or a class 84 | * extending it -- typically a class in the Zend\Mail\Protocol\Smtp\Auth 85 | * namespace. 86 | * 87 | * @return string 88 | */ 89 | public function getConnectionClass() 90 | { 91 | return $this->connectionClass; 92 | } 93 | 94 | /** 95 | * Set connection class 96 | * 97 | * @param string $connectionClass the value to be set 98 | * @throws \Zend\Mail\Exception\InvalidArgumentException 99 | * @return SmtpOptions 100 | */ 101 | public function setConnectionClass($connectionClass) 102 | { 103 | if (! is_string($connectionClass) && $connectionClass !== null) { 104 | throw new Exception\InvalidArgumentException(sprintf( 105 | 'Connection class must be a string or null; argument of type "%s" provided', 106 | (is_object($connectionClass) ? get_class($connectionClass) : gettype($connectionClass)) 107 | )); 108 | } 109 | $this->connectionClass = $connectionClass; 110 | return $this; 111 | } 112 | 113 | /** 114 | * Get connection configuration array 115 | * 116 | * @return array 117 | */ 118 | public function getConnectionConfig() 119 | { 120 | return $this->connectionConfig; 121 | } 122 | 123 | /** 124 | * Set connection configuration array 125 | * 126 | * @param array $connectionConfig 127 | * @return SmtpOptions 128 | */ 129 | public function setConnectionConfig(array $connectionConfig) 130 | { 131 | $this->connectionConfig = $connectionConfig; 132 | return $this; 133 | } 134 | 135 | /** 136 | * Get the host name 137 | * 138 | * @return string 139 | */ 140 | public function getHost() 141 | { 142 | return $this->host; 143 | } 144 | 145 | /** 146 | * Set the SMTP host 147 | * 148 | * @todo hostname/IP validation 149 | * @param string $host 150 | * @return SmtpOptions 151 | */ 152 | public function setHost($host) 153 | { 154 | $this->host = (string) $host; 155 | return $this; 156 | } 157 | 158 | /** 159 | * Get the port the SMTP server runs on 160 | * 161 | * @return int 162 | */ 163 | public function getPort() 164 | { 165 | return $this->port; 166 | } 167 | 168 | /** 169 | * Set the port the SMTP server runs on 170 | * 171 | * @param int $port 172 | * @throws \Zend\Mail\Exception\InvalidArgumentException 173 | * @return SmtpOptions 174 | */ 175 | public function setPort($port) 176 | { 177 | $port = (int) $port; 178 | if ($port < 1) { 179 | throw new Exception\InvalidArgumentException(sprintf( 180 | 'Port must be greater than 1; received "%d"', 181 | $port 182 | )); 183 | } 184 | $this->port = $port; 185 | return $this; 186 | } 187 | 188 | /** 189 | * @return int|null 190 | */ 191 | public function getConnectionTimeLimit() 192 | { 193 | return $this->connectionTimeLimit; 194 | } 195 | 196 | /** 197 | * @param int|null $seconds 198 | * @return self 199 | */ 200 | public function setConnectionTimeLimit($seconds) 201 | { 202 | $this->connectionTimeLimit = $seconds === null 203 | ? null 204 | : (int) $seconds; 205 | 206 | return $this; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Transport/TransportInterface.php: -------------------------------------------------------------------------------- 1 |