diff --git a/composer.json b/composer.json index cb3d69f6..25435c00 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "zendframework/zend-mail", + "name": "particlebits/zend-mail", "description": "provides generalized functionality to compose and send both text and MIME-compliant multipart e-mail messages", "license": "BSD-3-Clause", "keywords": [ @@ -12,10 +12,16 @@ "Zend\\Mail\\": "src/" } }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/particlebits/zend-mime" + } + ], "require": { "php": "^5.5 || ^7.0", "zendframework/zend-loader": "^2.5", - "zendframework/zend-mime": "^2.5", + "particlebits/zend-mime": "~2.5", "zendframework/zend-stdlib": "^2.7 || ^3.0", "zendframework/zend-validator": "^2.6" }, @@ -30,8 +36,6 @@ "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3 when using SMTP to deliver messages", "zendframework/zend-crypt": "Crammd5 support in SMTP Auth" }, - "minimum-stability": "dev", - "prefer-stable": true, "extra": { "branch-alias": { "dev-master": "2.7-dev", diff --git a/src/Address.php b/src/Address.php index 21409933..f3ab669c 100644 --- a/src/Address.php +++ b/src/Address.php @@ -27,7 +27,11 @@ class Address implements Address\AddressInterface */ public function __construct($email, $name = null) { - $emailAddressValidator = new EmailAddressValidator(Hostname::ALLOW_DNS | Hostname::ALLOW_LOCAL); + $emailAddressValidator = new EmailAddressValidator([ + 'allow' => Hostname::ALLOW_DNS | Hostname::ALLOW_LOCAL, + 'strict' => false + ]); + if (! is_string($email) || empty($email)) { throw new Exception\InvalidArgumentException('Email must be a valid email address'); } @@ -36,10 +40,12 @@ public function __construct($email, $name = null) throw new Exception\InvalidArgumentException('CRLF injection detected'); } + /* Ignoring this check if (! $emailAddressValidator->isValid($email)) { $invalidMessages = $emailAddressValidator->getMessages(); throw new Exception\InvalidArgumentException(array_shift($invalidMessages)); } + */ if (null !== $name) { if (! is_string($name)) { diff --git a/src/Header/AbstractAddressList.php b/src/Header/AbstractAddressList.php index 7c8ce2c7..8a4110bb 100644 --- a/src/Header/AbstractAddressList.php +++ b/src/Header/AbstractAddressList.php @@ -42,6 +42,8 @@ abstract class AbstractAddressList implements HeaderInterface public static function fromString($headerLine) { list($fieldName, $fieldValue) = GenericHeader::splitHeaderLine($headerLine); + // clean up acceptable values + $fieldName = static::cleanFieldName($fieldName); if (strtolower($fieldName) !== static::$type) { throw new Exception\InvalidArgumentException(sprintf( 'Invalid header line for "%s" string', @@ -206,4 +208,10 @@ protected static function stripComments($value) $value ); } + + // Implemented in sub-classes + protected static function cleanFieldName($fieldName) + { + return $fieldName; + } } diff --git a/src/Header/ContentTransferEncoding.php b/src/Header/ContentTransferEncoding.php index 687230c9..3d9c5cea 100644 --- a/src/Header/ContentTransferEncoding.php +++ b/src/Header/ContentTransferEncoding.php @@ -91,6 +91,8 @@ public function setTransferEncoding($transferEncoding) { // Per RFC 1521, the value of the header is not case sensitive $transferEncoding = strtolower($transferEncoding); + // Remove trailing semicolon + $transferEncoding = rtrim($transferEncoding, ';'); if (! in_array($transferEncoding, static::$allowedTransferEncodings)) { throw new Exception\InvalidArgumentException(sprintf( diff --git a/src/Header/ContentType.php b/src/Header/ContentType.php index 3e2f48ba..878b2699 100644 --- a/src/Header/ContentType.php +++ b/src/Header/ContentType.php @@ -52,7 +52,9 @@ public static function fromString($headerLine) $values = array_filter($values); foreach ($values as $keyValuePair) { - list($key, $value) = explode('=', $keyValuePair, 2); + $parts = explode('=', $keyValuePair, 2); + $key = $parts[0]; + $value = (count($parts) > 1) ? $parts[1] : ""; $value = trim($value, "'\" \t\n\r\0\x0B"); $header->addParameter($key, $value); } @@ -106,12 +108,15 @@ public function toString() * Set the content type * * @param string $type + * @param bool $throwExceptionOnInvalid * @throws Exception\InvalidArgumentException * @return ContentType */ - public function setType($type) + public function setType($type, $throwExceptionOnInvalid = false) { - if (! preg_match('/^[a-z-]+\/[a-z0-9.+-]+$/i', $type)) { + if (! preg_match('/^[a-z-]+\/[a-z0-9.+-]+$/i', $type) + && $throwExceptionOnInvalid === true) + { throw new Exception\InvalidArgumentException(sprintf( '%s expects a value in the format "type/subtype"; received "%s"', __METHOD__, diff --git a/src/Header/GenericHeader.php b/src/Header/GenericHeader.php index d87d51e6..fe073869 100644 --- a/src/Header/GenericHeader.php +++ b/src/Header/GenericHeader.php @@ -46,7 +46,7 @@ public static function fromString($headerLine) * @return string[] `name` in the first index and `value` in the second. * @throws Exception\InvalidArgumentException If header does not match with the format ``name:value`` */ - public static function splitHeaderLine($headerLine) + public static function splitHeaderLine($headerLine, $throwExceptionOnInvalid = false) { $parts = explode(':', $headerLine, 2); if (count($parts) !== 2) { @@ -54,11 +54,17 @@ public static function splitHeaderLine($headerLine) } if (! HeaderName::isValid($parts[0])) { - throw new Exception\InvalidArgumentException('Invalid header name detected'); + if ( $throwExceptionOnInvalid === true) { + throw new Exception\InvalidArgumentException('Invalid header name detected'); + } + $parts[0] = HeaderName::filter($parts[0]); } if (! HeaderValue::isValid($parts[1])) { - throw new Exception\InvalidArgumentException('Invalid header value detected'); + if ($throwExceptionOnInvalid === true) { + throw new Exception\InvalidArgumentException('Invalid header value detected'); + } + $parts[1] = HeaderValue::filter($parts[1]); } $parts[0] = $parts[0]; diff --git a/src/Header/HeaderWrap.php b/src/Header/HeaderWrap.php index 2a0b838f..a7699dc0 100644 --- a/src/Header/HeaderWrap.php +++ b/src/Header/HeaderWrap.php @@ -124,13 +124,16 @@ public static function canBeEncoded($value) $lineLength = strlen($value) * 4 + strlen($charset) + 16; $preferences = [ - 'scheme' => 'Q', + 'scheme' => 'B', 'input-charset' => $charset, 'output-charset' => $charset, 'line-length' => $lineLength, ]; - $encoded = iconv_mime_encode('x-test', $value, $preferences); + $encodedB = @iconv_mime_encode('x-test', $value, $preferences); + $preferences['scheme'] = 'Q'; + $encodedQ = @iconv_mime_encode('x-test', $value, $preferences); + $encoded = $encodedB || $encodedQ; return (false !== $encoded); } diff --git a/src/Header/MessageId.php b/src/Header/MessageId.php index ae8e12e9..cd6977c7 100644 --- a/src/Header/MessageId.php +++ b/src/Header/MessageId.php @@ -76,7 +76,7 @@ public function setId($id = null) throw new Exception\InvalidArgumentException('Invalid ID detected'); } - $this->messageId = sprintf('<%s>', $id); + $this->messageId = sprintf('<%s>', trim($id, "<>")); return $this; } diff --git a/src/Header/MimeVersion.php b/src/Header/MimeVersion.php index 738a1ed7..70682a8b 100644 --- a/src/Header/MimeVersion.php +++ b/src/Header/MimeVersion.php @@ -22,7 +22,7 @@ public static function fromString($headerLine) $value = HeaderWrap::mimeDecodeValue($value); // check to ensure proper header type for this factory - if (strtolower($name) !== 'mime-version') { + if (strtolower($name) !== 'mime-version' && strtolower($name) !== 'mimeversion') { throw new Exception\InvalidArgumentException('Invalid header line for MIME-Version string'); } diff --git a/src/Header/ReplyTo.php b/src/Header/ReplyTo.php index 25cb2801..ad3736e6 100644 --- a/src/Header/ReplyTo.php +++ b/src/Header/ReplyTo.php @@ -13,4 +13,19 @@ class ReplyTo extends AbstractAddressList { protected $fieldName = 'Reply-To'; protected static $type = 'reply-to'; + + protected static function cleanFieldName($fieldName) + { + $allowed = [ + 'replyto', 'reply_to' + ]; + + foreach ($allowed as $name) { + if (strtolower($fieldName) === $name) { + return static::$type; + } + } + + return $fieldName; + } } diff --git a/src/Protocol/Imap.php b/src/Protocol/Imap.php index 85aadf45..d08e21c7 100644 --- a/src/Protocol/Imap.php +++ b/src/Protocol/Imap.php @@ -552,6 +552,9 @@ public function fetch($items, $from, $to = null, $uid = false) $tag = null; // define $tag variable before first use $this->sendRequest(($uid ? 'UID ' : '') . 'FETCH', [$set, $itemList], $tag); + // remove any peek lines since the response doesn't return this + $items[0] = str_replace('.PEEK', '', $items[0]); + $result = []; $tokens = null; // define $tokens variable before first use while (! $this->readLine($tokens, $tag)) { @@ -808,13 +811,16 @@ public function noop() * * This method is currently marked as internal as the API might change and is not * safe if you don't take precautions. - * + * If $uid is true then this will return UIDs instead of message IDs. * @param array $params - * @return array message ids + * @param bool $uid + * @return array message ids|unique ids */ - public function search(array $params) + public function search(array $params, $uid = true) { - $response = $this->requestAndResponse('SEARCH', $params); + $command = ($uid) ? 'UID SEARCH' : 'SEARCH'; + $response = $this->requestAndResponse($command, $params); + if (! $response) { return $response; } diff --git a/src/Storage/Imap.php b/src/Storage/Imap.php index 903603de..a186da69 100644 --- a/src/Storage/Imap.php +++ b/src/Storage/Imap.php @@ -112,13 +112,19 @@ public function getSize($id = 0) * Fetch a message * * @param int $id number of message + * @param bool $peek true leaves the message unread * @return Message * @throws Protocol\Exception\RuntimeException */ - public function getMessage($id) + public function getMessage($id, $peek = true) { - $data = $this->protocol->fetch(['FLAGS', 'RFC822.HEADER'], $id); - $header = $data['RFC822.HEADER']; + //$data = $this->protocol->fetch(['FLAGS', 'RFC822.HEADER'], $id); + $data = ($peek) + ? $this->protocol->fetch(['FLAGS', 'BODY.PEEK[HEADER]'], $id) + : $this->protocol->fetch(['FLAGS', 'RFC822.HEADER'], $id); + $header = ($peek) + ? $data['BODY[HEADER]'] + : $data['RFC822.HEADER']; $flags = []; foreach ($data['FLAGS'] as $flag) { @@ -155,18 +161,21 @@ public function getRawHeader($id, $part = null, $topLines = 0) * * @param int $id number of message * @param null|array|string $part path to part or null for message content + * @param bool $peek true leaves the message unread * @return string raw content * @throws Protocol\Exception\RuntimeException * @throws Exception\RuntimeException */ - public function getRawContent($id, $part = null) + public function getRawContent($id, $part = null, $peek = true) { if ($part !== null) { // TODO: implement throw new Exception\RuntimeException('not implemented'); } - return $this->protocol->fetch('RFC822.TEXT', $id); + return ($peek) + ? $this->protocol->fetch('BODY.PEEK[TEXT]', $id) + : $this->protocol->fetch('RFC822.TEXT', $id); } /** diff --git a/src/Storage/Part.php b/src/Storage/Part.php index 3e75dee7..363abf12 100644 --- a/src/Storage/Part.php +++ b/src/Storage/Part.php @@ -113,6 +113,9 @@ public function __construct(array $params) } else { $this->headers = Headers::fromString($params['headers']); } + if (!$this->headers) { + $this->headers = new Headers(); + } } if (isset($params['content'])) {