This commit is contained in:
cutemeli
2025-12-22 10:35:30 +00:00
parent 0bfc6c8425
commit 5ce7ca2c5d
38927 changed files with 0 additions and 4594700 deletions

View File

@@ -1 +0,0 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/)

View File

@@ -1,26 +0,0 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of Laminas Foundation nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,12 +0,0 @@
# laminas-mail
[![Build Status](https://github.com/laminas/laminas-mail/workflows/Continuous%20Integration/badge.svg)](https://github.com/laminas/laminas-mail/actions?query=workflow%3A"Continuous+Integration")
`Laminas\Mail` provides generalized functionality to compose and send both text and
MIME-compliant multipart email messages. Mail can be sent with `Laminas\Mail` via
the `Mail\Transport\Sendmail`, `Mail\Transport\Smtp` or the `Mail\Transport\File`
transport. Of course, you can also implement your own transport by implementing
the `Mail\Transport\TransportInterface`.
- File issues at https://github.com/laminas/laminas-mail/issues
- Documentation is at https://docs.laminas.dev/laminas-mail/

View File

@@ -1,79 +0,0 @@
{
"name": "plesk/laminas-mail",
"description": "Provides generalized functionality to compose and send both text and MIME-compliant multipart e-mail messages",
"keywords": [
"laminas",
"mail"
],
"homepage": "https://laminas.dev",
"license": "BSD-3-Clause",
"require": {
"php": "^8.0",
"ext-iconv": "*",
"laminas/laminas-loader": "^2.9.0",
"plesk/laminas-mime": "2.13.0-patch1",
"laminas/laminas-stdlib": "^3.17.0",
"laminas/laminas-validator": "^2.31.0",
"symfony/polyfill-mbstring": "^1.27.0",
"webmozart/assert": "^1.11.0",
"symfony/polyfill-intl-idn": "^1.27.0"
},
"require-dev": {
"laminas/laminas-coding-standard": "~2.5.0",
"laminas/laminas-db": "^2.18",
"laminas/laminas-servicemanager": "^3.22.1",
"phpunit/phpunit": "^10.4.2",
"psalm/plugin-phpunit": "^0.18.4",
"symfony/process": "^6.3.4 || ^7.0.0",
"vimeo/psalm": "^5.15"
},
"suggest": {
"laminas/laminas-servicemanager": "^3.21 when using SMTP to deliver messages"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"composer/package-versions-deprecated": true,
"dealerdirect/phpcodesniffer-composer-installer": true
},
"platform": {
"php": "8.1.99"
}
},
"extra": {
"laminas": {
"component": "Laminas\\Mail",
"config-provider": "Laminas\\Mail\\ConfigProvider"
}
},
"autoload": {
"psr-4": {
"Laminas\\Mail\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LaminasTest\\Mail\\": "test/"
}
},
"scripts": {
"check": [
"@cs-check",
"@static-analysis",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"static-analysis": "psalm --shepherd --stats",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
},
"support": {
"issues": "https://github.com/laminas/laminas-mail/issues",
"forum": "https://discourse.laminas.dev",
"chat": "https://laminas.dev/chat",
"source": "https://github.com/laminas/laminas-mail",
"docs": "https://docs.laminas.dev/laminas-mail/",
"rss": "https://github.com/laminas/laminas-mail/releases.atom"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,171 +0,0 @@
<?php
namespace Laminas\Mail;
use Laminas\Validator\EmailAddress as EmailAddressValidator;
use Laminas\Validator\Hostname;
use function array_shift;
use function is_string;
use function preg_match;
use function sprintf;
use function trim;
class Address implements Address\AddressInterface
{
/** @var null|string */
protected $comment;
/** @var string */
protected $email;
/** @var null|string */
protected $name;
/**
* Create an instance from a string value.
*
* Parses a string representing a single address. If it is a valid format,
* it then creates and returns an instance of itself using the name and
* email it has parsed from the value.
*
* @param string $address
* @param null|string $comment Comment associated with the address, if any.
* @throws Exception\InvalidArgumentException
* @return self
*/
public static function fromString($address, $comment = null)
{
if (! preg_match('/^((?P<name>.*)<(?P<namedEmail>[^>]+)>|(?P<email>.+))$/', $address, $matches)) {
throw new Exception\InvalidArgumentException('Invalid address format');
}
$name = null;
if (isset($matches['name'])) {
$name = trim($matches['name']);
}
if (empty($name)) {
$name = null;
}
if (isset($matches['namedEmail'])) {
$email = $matches['namedEmail'];
}
if (isset($matches['email'])) {
$email = $matches['email'];
}
$email = trim($email);
//trim single quotes, because outlook does add single quotes to emails sometimes which is technically not valid
$email = trim($email, '\'');
return new static($email, $name, $comment);
}
/**
* Constructor
*
* @param string $email
* @param null|string $name
* @param null|string $comment
* @throws Exception\InvalidArgumentException
*/
public function __construct($email, $name = null, $comment = null)
{
$emailAddressValidator = new EmailAddressValidator(Hostname::ALLOW_DNS | Hostname::ALLOW_LOCAL);
if (! is_string($email) || empty($email)) {
throw new Exception\InvalidArgumentException('Email must be a valid email address');
}
if (preg_match("/[\r\n]/", $email)) {
throw new Exception\InvalidArgumentException('CRLF injection detected');
}
if (! $emailAddressValidator->isValid($email)) {
$invalidMessages = $emailAddressValidator->getMessages();
throw new Exception\InvalidArgumentException(array_shift($invalidMessages));
}
if (null !== $name) {
if (! is_string($name)) {
throw new Exception\InvalidArgumentException('Name must be a string');
}
if (preg_match("/[\r\n]/", $name)) {
throw new Exception\InvalidArgumentException('CRLF injection detected');
}
$this->name = $name;
}
$this->email = $email;
if (null !== $comment) {
$this->comment = $comment;
}
}
/**
* Retrieve email
*
* @return string
*/
public function getEmail()
{
return $this->email;
}
/**
* Retrieve name, if any
*
* @return null|string
*/
public function getName()
{
return $this->name;
}
/**
* Retrieve comment, if any
*
* @return null|string
*/
public function getComment()
{
return $this->comment;
}
/**
* String representation of address
*
* @return string
*/
public function toString()
{
$string = sprintf('<%s>', $this->getEmail());
$name = $this->constructName();
if (null === $name) {
return $string;
}
return sprintf('%s %s', $name, $string);
}
/**
* Constructs the name string
*
* If a comment is present, appends the comment (commented using parens) to
* the name before returning it; otherwise, returns just the name.
*
* @return null|string
*/
private function constructName()
{
$name = $this->getName();
$comment = $this->getComment();
if ($comment === null || $comment === '') {
return $name;
}
$string = sprintf('%s (%s)', $name, $comment);
return trim($string);
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace Laminas\Mail\Address;
interface AddressInterface
{
/**
* Retrieve email
*
* @return string
*/
public function getEmail();
/**
* Retrieve name, if any
*
* @return null|string
*/
public function getName();
/**
* String representation of address
*
* @return string
*/
public function toString();
}

View File

@@ -1,258 +0,0 @@
<?php
namespace Laminas\Mail;
use Countable;
use Iterator;
use Laminas\Mail\Address\AddressInterface;
use ReturnTypeWillChange;
use function count;
use function current;
use function gettype;
use function is_int;
use function is_numeric;
use function is_object;
use function is_string;
use function key;
use function next;
use function reset;
use function sprintf;
use function strtolower;
use function var_export;
/**
* @implements Iterator<string, AddressInterface>
* @final
*/
class AddressList implements Countable, Iterator
{
/**
* List of Address objects we're managing
*
* @var array<string, AddressInterface>
*/
protected $addresses = [];
/**
* Add an address to the list
*
* @param string|AddressInterface $emailOrAddress
* @param null|string $name
* @throws Exception\InvalidArgumentException
* @return $this
*/
public function add($emailOrAddress, $name = null)
{
if (is_string($emailOrAddress)) {
$emailOrAddress = $this->createAddress($emailOrAddress, $name);
}
if (! $emailOrAddress instanceof AddressInterface) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects an email address or %s\Address object as its first argument; received "%s"',
__METHOD__,
__NAMESPACE__,
is_object($emailOrAddress) ? $emailOrAddress::class : gettype($emailOrAddress)
));
}
$email = strtolower($emailOrAddress->getEmail());
if ($this->has($email)) {
return $this;
}
$this->addresses[$email] = $emailOrAddress;
return $this;
}
/**
* Add many addresses at once
*
* If an email key is provided, it will be used as the email, and the value
* as the name. Otherwise, the value is passed as the sole argument to add(),
* and, as such, can be either email strings or Address\AddressInterface objects.
*
* @param array $addresses
* @throws Exception\RuntimeException
* @return $this
*/
public function addMany(array $addresses)
{
foreach ($addresses as $key => $value) {
if (is_int($key) || is_numeric($key)) {
$this->add($value);
continue;
}
if (! is_string($key)) {
throw new Exception\RuntimeException(sprintf(
'Invalid key type in provided addresses array ("%s")',
is_object($key) ? $key::class : var_export($key, true)
));
}
$this->add($key, $value);
}
return $this;
}
/**
* Add an address to the list from any valid string format, such as
* - "Laminas Dev" <dev@laminas.com>
* - dev@laminas.com
*
* @param string $address
* @param null|string $comment Comment associated with the address, if any.
* @throws Exception\InvalidArgumentException
* @return $this
*/
public function addFromString($address, $comment = null)
{
$this->add(Address::fromString($address, $comment));
return $this;
}
/**
* Merge another address list into this one
*
* @return $this
*/
public function merge(self $addressList)
{
foreach ($addressList as $address) {
$this->add($address);
}
return $this;
}
/**
* Does the email exist in this list?
*
* @param string $email
* @return bool
*/
public function has($email)
{
$email = strtolower($email);
return isset($this->addresses[$email]);
}
/**
* Get an address by email
*
* @param string $email
* @return false|AddressInterface
*/
public function get($email)
{
$email = strtolower($email);
if (! isset($this->addresses[$email])) {
return false;
}
return $this->addresses[$email];
}
/**
* Delete an address from the list
*
* @param string $email
* @return bool
*/
public function delete($email)
{
$email = strtolower($email);
if (! isset($this->addresses[$email])) {
return false;
}
unset($this->addresses[$email]);
return true;
}
/**
* Return count of addresses
*
* @return int
*/
#[ReturnTypeWillChange]
public function count()
{
return count($this->addresses);
}
/**
* Rewind iterator
*
* @see addresses
*
* @return false|AddressInterface the value of the first addresses element, or false if the addresses is
* empty.
*/
#[ReturnTypeWillChange]
public function rewind()
{
return reset($this->addresses);
}
/**
* Return current item in iteration
*
* @return AddressInterface
*/
#[ReturnTypeWillChange]
public function current()
{
return current($this->addresses);
}
/**
* Return key of current item of iteration
*
* @return string
*/
#[ReturnTypeWillChange]
public function key()
{
return key($this->addresses);
}
/**
* Move to next item
*
* @see addresses
*
* @return false|AddressInterface the addresses value in the next place that's pointed to by the
* internal array pointer, or false if there are no more elements.
*/
#[ReturnTypeWillChange]
public function next()
{
return next($this->addresses);
}
/**
* Is the current item of iteration valid?
*
* @return bool
*/
#[ReturnTypeWillChange]
public function valid()
{
$key = key($this->addresses);
return $key !== null && $key !== false;
}
/**
* Create an address object
*
* @param string $email
* @param string|null $name
* @return Address
*/
protected function createAddress($email, $name)
{
return new Address($email, $name);
}
}

View File

@@ -1,36 +0,0 @@
<?php
namespace Laminas\Mail;
class ConfigProvider
{
/**
* Retrieve configuration for laminas-mail package.
*
* @return array
*/
public function __invoke()
{
return [
'dependencies' => $this->getDependencyConfig(),
];
}
/**
* Retrieve dependency settings for laminas-mail package.
*
* @return array
*/
public function getDependencyConfig()
{
return [
// Legacy Zend Framework aliases
'aliases' => [
'Zend\Mail\Protocol\SmtpPluginManager' => Protocol\SmtpPluginManager::class,
],
'factories' => [
Protocol\SmtpPluginManager::class => Protocol\SmtpPluginManagerFactory::class,
],
];
}
}

View File

@@ -1,11 +0,0 @@
<?php
namespace Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class BadMethodCallException extends \BadMethodCallException implements
ExceptionInterface
{
}

View File

@@ -1,10 +0,0 @@
<?php
namespace Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class DomainException extends \DomainException implements ExceptionInterface
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Mail\Exception;
use Throwable;
interface ExceptionInterface extends Throwable
{
}

View File

@@ -1,11 +0,0 @@
<?php
namespace Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class InvalidArgumentException extends \InvalidArgumentException implements
ExceptionInterface
{
}

View File

@@ -1,10 +0,0 @@
<?php
namespace Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
{
}

View File

@@ -1,10 +0,0 @@
<?php
namespace Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -1,323 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use Laminas\Mail\Address;
use Laminas\Mail\AddressList;
use Laminas\Mail\Headers;
use Laminas\Mail\Storage\Exception\RuntimeException;
use function array_filter;
use function array_map;
use function assert;
use function idn_to_ascii;
use function implode;
use function in_array;
use function is_array;
use function is_string;
use function preg_match;
use function preg_match_all;
use function preg_replace;
use function sprintf;
use function str_contains;
use function str_replace;
use function strtolower;
use function trim;
use const IDNA_DEFAULT;
use const IDNA_ERROR_BIDI;
use const IDNA_ERROR_CONTEXTJ;
use const IDNA_ERROR_DISALLOWED;
use const IDNA_ERROR_DOMAIN_NAME_TOO_LONG;
use const IDNA_ERROR_EMPTY_LABEL;
use const IDNA_ERROR_HYPHEN_3_4;
use const IDNA_ERROR_INVALID_ACE_LABEL;
use const IDNA_ERROR_LABEL_HAS_DOT;
use const IDNA_ERROR_LABEL_TOO_LONG;
use const IDNA_ERROR_LEADING_COMBINING_MARK;
use const IDNA_ERROR_LEADING_HYPHEN;
use const IDNA_ERROR_PUNYCODE;
use const IDNA_ERROR_TRAILING_HYPHEN;
use const INTL_IDNA_VARIANT_UTS46;
/**
* Base class for headers composing address lists (to, from, cc, bcc, reply-to)
*/
abstract class AbstractAddressList implements HeaderInterface
{
private const IDNA_ERROR_MAP = [
IDNA_ERROR_EMPTY_LABEL => 'empty label',
IDNA_ERROR_LABEL_TOO_LONG => 'label too long',
IDNA_ERROR_DOMAIN_NAME_TOO_LONG => 'domain name too long',
IDNA_ERROR_LEADING_HYPHEN => 'leading hyphen',
IDNA_ERROR_TRAILING_HYPHEN => 'trailing hyphen',
IDNA_ERROR_HYPHEN_3_4 => 'consecutive hyphens',
IDNA_ERROR_LEADING_COMBINING_MARK => 'leading combining mark',
IDNA_ERROR_DISALLOWED => 'disallowed',
IDNA_ERROR_PUNYCODE => 'invalid punycode encoding',
IDNA_ERROR_LABEL_HAS_DOT => 'has dot',
IDNA_ERROR_INVALID_ACE_LABEL => 'label not in ASCII encoding',
IDNA_ERROR_BIDI => 'fails bidirectional criteria',
IDNA_ERROR_CONTEXTJ => 'one or more characters fail CONTEXTJ rule',
];
/** @var AddressList */
protected $addressList;
/** @var string Normalized field name */
protected $fieldName;
/**
* Header encoding
*
* @var string
*/
protected $encoding = 'ASCII';
/** @var string lower case field name */
protected static $type;
/** @var string[] lower case aliases for the field name */
protected static $typeAliases = [];
/**
* @param string $headerLine
* @return static
*/
public static function fromString($headerLine)
{
[$fieldName, $fieldValue] = GenericHeader::splitHeaderLine($headerLine);
if ((strtolower($fieldName) !== static::$type) && ! in_array(strtolower($fieldName), static::$typeAliases)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid header line for "%s" string',
self::class
));
}
// split value on ","
$fieldValue = str_replace(Headers::FOLDING, ' ', $fieldValue);
$fieldValue = preg_replace('/[^:]+:([^;]*);/', '$1,', $fieldValue);
$values = ListParser::parse($fieldValue);
$wasEncoded = false;
$addresses = array_map(
static function ($value) use (&$wasEncoded): ?Address {
$decodedValue = HeaderWrap::mimeDecodeValue($value);
$wasEncoded = $wasEncoded || ($decodedValue !== $value);
$value = trim($decodedValue);
$comments = self::getComments($value);
$value = self::stripComments($value);
$value = preg_replace(
[
'#(?<!\\\)"(.*)(?<!\\\)"#', // quoted-text
'#\\\([\x01-\x09\x0b\x0c\x0e-\x7f])#', // quoted-pair
],
[
'\\1',
'\\1',
],
$value
);
return empty($value) ? null : Address::fromString($value, $comments);
},
$values
);
$addresses = array_filter($addresses);
$header = new static();
if ($wasEncoded) {
$header->setEncoding('UTF-8');
}
/** @var AddressList $addressList */
$addressList = $header->getAddressList();
foreach ($addresses as $address) {
$addressList->add($address);
}
return $header;
}
/**
* @return string
*/
public function getFieldName()
{
return $this->fieldName;
}
/**
* Safely convert UTF-8 encoded domain name to ASCII
*
* @param string $domainName the UTF-8 encoded email
*/
protected function idnToAscii($domainName): string
{
/** @psalm-var string|false $ascii */
$ascii = idn_to_ascii($domainName, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $conversionInfo);
if (is_string($ascii)) {
return $ascii;
}
$messages = [];
assert(is_array($conversionInfo));
/* @psalm-var array{errors: numeric-string} $conversionInfo */
$errors = (int) $conversionInfo['errors'];
foreach (self::IDNA_ERROR_MAP as $flag => $message) {
if (($flag & $errors) === $flag) {
$messages[] = $message;
}
}
throw new RuntimeException(sprintf(
'Failed encoding domain due to errors: %s',
implode(', ', $messages)
));
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
$emails = [];
$encoding = $this->getEncoding();
foreach ($this->getAddressList() as $address) {
$email = $address->getEmail();
$name = $address->getName();
// quote $name if value requires so
if (! empty($name) && (str_contains($name, ',') || str_contains($name, ';'))) {
// FIXME: what if name contains double quote?
$name = sprintf('"%s"', $name);
}
if (
$format === HeaderInterface::FORMAT_ENCODED
&& 'ASCII' !== $encoding
) {
if (! empty($name)) {
$name = HeaderWrap::mimeEncodeValue($name, $encoding);
}
if (preg_match('/^(.+)@([^@]+)$/', $email, $matches)) {
$localPart = $matches[1];
$hostname = $this->idnToAscii($matches[2]);
$email = sprintf('%s@%s', $localPart, $hostname);
}
}
if (empty($name)) {
$emails[] = $email;
} else {
$emails[] = sprintf('%s <%s>', $name, $email);
}
}
// Ensure the values are valid before sending them.
if ($format !== HeaderInterface::FORMAT_RAW) {
foreach ($emails as $email) {
HeaderValue::assertValid($email);
}
}
return implode(',' . Headers::FOLDING, $emails);
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
{
$this->encoding = $encoding;
return $this;
}
/**
* @return string
*/
public function getEncoding()
{
return $this->encoding;
}
/**
* Set address list for this header
*/
public function setAddressList(AddressList $addressList)
{
$this->addressList = $addressList;
}
/**
* Get address list managed by this header
*
* @return AddressList
*/
public function getAddressList()
{
if (null === $this->addressList) {
$this->setAddressList(new AddressList());
}
return $this->addressList;
}
/**
* @return string
*/
public function toString()
{
$name = $this->getFieldName();
$value = $this->getFieldValue(HeaderInterface::FORMAT_ENCODED);
return empty($value) ? '' : sprintf('%s: %s', $name, $value);
}
/**
* Retrieve comments from value, if any.
*
* Supposed to be private, protected as a workaround for PHP bug 68194
*
* @param string $value
* @return string
*/
protected static function getComments($value)
{
$matches = [];
preg_match_all(
'/\\(
(?P<comment>(
\\\\.|
[^\\\\)]
)+)
\\)/x',
$value,
$matches
);
return isset($matches['comment']) ? implode(', ', $matches['comment']) : '';
}
/**
* Strip all comments from value, if any.
*
* Supposed to be private, protected as a workaround for PHP bug 68194
*
* @param string $value
* @return string
*/
protected static function stripComments($value)
{
return preg_replace(
'/\\(
(
\\\\.|
[^\\\\)]
)+
\\)/x',
'',
$value
);
}
}

View File

@@ -1,12 +0,0 @@
<?php
namespace Laminas\Mail\Header;
class Bcc extends AbstractAddressList
{
/** @var string */
protected $fieldName = 'Bcc';
/** @var string */
protected static $type = 'bcc';
}

View File

@@ -1,11 +0,0 @@
<?php
namespace Laminas\Mail\Header;
class Cc extends AbstractAddressList
{
/** @var string */
protected $fieldName = 'Cc';
/** @var string */
protected static $type = 'cc';
}

View File

@@ -1,326 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use Laminas\Mail\Headers;
use Laminas\Mime\Mime;
use function count;
use function explode;
use function gettype;
use function in_array;
use function is_numeric;
use function mb_strlen;
use function mb_substr;
use function sprintf;
use function str_replace;
use function strlen;
use function strpos;
use function strtolower;
use function trim;
use function var_export;
class ContentDisposition implements UnstructuredInterface
{
/**
* 78 chars (RFC 2822) - (semicolon + space (Header::FOLDING))
*
* @var int
*/
public const MAX_PARAMETER_LENGTH = 76;
/** @var string */
protected $disposition = 'inline';
/**
* Header encoding
*
* @var string
*/
protected $encoding = 'ASCII';
/** @var array */
protected $parameters = [];
/**
* @inheritDoc
*/
public static function fromString($headerLine)
{
[$name, $value] = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (! in_array(strtolower($name), ['contentdisposition', 'content_disposition', 'content-disposition'])) {
throw new Exception\InvalidArgumentException('Invalid header line for Content-Disposition string');
}
$value = str_replace(Headers::FOLDING, ' ', $value);
$parts = explode(';', $value, 2);
$header = new static();
$header->setDisposition($parts[0]);
if (isset($parts[1])) {
$values = ListParser::parse(trim($parts[1]), [';', '=']);
$length = count($values);
$continuedValues = [];
for ($i = 0; $i < $length; $i += 2) {
$value = $values[$i + 1];
$value = trim($value, "'\" \t\n\r\0\x0B");
$name = trim($values[$i], "'\" \t\n\r\0\x0B");
if (strpos($name, '*')) {
[$name, $count] = explode('*', $name);
// allow optional count:
// Content-Disposition: attachment; filename*=UTF-8''%64%61%61%6D%69%2D%6D%C3%B5%72%76%2E%6A%70%67
if ($count === "") {
$count = 0;
}
if (! is_numeric($count)) {
$type = gettype($count);
$value = var_export($count, true);
throw new Exception\InvalidArgumentException(sprintf(
"Invalid header line for Content-Disposition string"
. " - count expected to be numeric, got %s with value %s",
$type,
$value
));
}
if (! isset($continuedValues[$name])) {
$continuedValues[$name] = [];
}
$continuedValues[$name][$count] = $value;
} else {
$header->setParameter($name, $value);
}
}
foreach ($continuedValues as $name => $values) {
$value = '';
for ($i = 0, $iMax = count($values); $i < $iMax; $i++) {
if (! isset($values[$i])) {
throw new Exception\InvalidArgumentException(
'Invalid header line for Content-Disposition string - incomplete continuation'
. '; HeaderLine: ' . $headerLine
);
}
$value .= $values[$i];
}
$header->setParameter($name, $value);
}
}
return $header;
}
/**
* @inheritDoc
*/
public function getFieldName()
{
return 'Content-Disposition';
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
$result = $this->disposition;
if (empty($this->parameters)) {
return $result;
}
foreach ($this->parameters as $attribute => $value) {
$valueIsEncoded = false;
if (HeaderInterface::FORMAT_ENCODED === $format && ! Mime::isPrintable($value)) {
$value = $this->getEncodedValue($value);
$valueIsEncoded = true;
}
$line = sprintf('%s="%s"', $attribute, $value);
if (strlen($line) < self::MAX_PARAMETER_LENGTH) {
$lines = explode(Headers::FOLDING, $result);
if (count($lines) === 1) {
$existingLineLength = strlen('Content-Disposition: ' . $result);
} else {
$existingLineLength = 1 + strlen($lines[count($lines) - 1]);
}
if ((2 + $existingLineLength + strlen($line)) <= self::MAX_PARAMETER_LENGTH) {
$result .= '; ' . $line;
} else {
$result .= ';' . Headers::FOLDING . $line;
}
} else {
// Use 'continuation' per RFC 2231
if ($valueIsEncoded) {
$value = HeaderWrap::mimeDecodeValue($value);
}
$i = 0;
$fullLength = mb_strlen($value, 'UTF-8');
while ($fullLength > 0) {
$attributePart = $attribute . '*' . $i++ . '="';
$attLen = mb_strlen($attributePart, 'UTF-8');
$subPos = 1;
$valuePart = '';
while ($subPos <= $fullLength) {
$sub = mb_substr($value, 0, $subPos, 'UTF-8');
if ($valueIsEncoded) {
$sub = $this->getEncodedValue($sub);
}
if ($attLen + mb_strlen($sub, 'UTF-8') >= self::MAX_PARAMETER_LENGTH) {
$subPos--;
break;
}
$subPos++;
$valuePart = $sub;
}
$value = mb_substr($value, $subPos, null, 'UTF-8');
$fullLength = mb_strlen($value, 'UTF-8');
$result .= ';' . Headers::FOLDING . $attributePart . $valuePart . '"';
}
}
}
return $result;
}
/**
* @param string $value
* @return string
*/
protected function getEncodedValue($value)
{
$configuredEncoding = $this->encoding;
$this->encoding = 'UTF-8';
$value = HeaderWrap::wrap($value, $this);
$this->encoding = $configuredEncoding;
return $value;
}
/**
* @inheritDoc
*/
public function setEncoding($encoding)
{
$this->encoding = $encoding;
return $this;
}
/**
* @inheritDoc
*/
public function getEncoding()
{
return $this->encoding;
}
/**
* @inheritDoc
*/
public function toString()
{
return 'Content-Disposition: ' . $this->getFieldValue(HeaderInterface::FORMAT_ENCODED);
}
/**
* Set the content disposition
* Expected values include 'inline', 'attachment'
*
* @param string $disposition
* @return ContentDisposition
*/
public function setDisposition($disposition)
{
$this->disposition = strtolower($disposition);
return $this;
}
/**
* Retrieve the content disposition
*
* @return string
*/
public function getDisposition()
{
return $this->disposition;
}
/**
* Add a parameter pair
*
* @param string $name
* @param string $value
* @return ContentDisposition
*/
public function setParameter($name, $value)
{
$name = strtolower($name);
if (! HeaderValue::isValid($name)) {
throw new Exception\InvalidArgumentException(
'Invalid content-disposition parameter name detected'
);
}
// '5' here is for the quotes & equal sign in `name="value"`,
// and the space & semicolon for line folding
if ((strlen($name) + 5) >= self::MAX_PARAMETER_LENGTH) {
throw new Exception\InvalidArgumentException(
'Invalid content-disposition parameter name detected (too long)'
);
}
$this->parameters[$name] = $value;
return $this;
}
/**
* Get all parameters
*
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Get a parameter by name
*
* @param string $name
* @return null|string
*/
public function getParameter($name)
{
$name = strtolower($name);
if (isset($this->parameters[$name])) {
return $this->parameters[$name];
}
return null;
}
/**
* Remove a named parameter
*
* @param string $name
* @return bool
*/
public function removeParameter($name)
{
$name = strtolower($name);
if (isset($this->parameters[$name])) {
unset($this->parameters[$name]);
return true;
}
return false;
}
}

View File

@@ -1,135 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use function implode;
use function in_array;
use function sprintf;
use function strtolower;
class ContentTransferEncoding implements HeaderInterface
{
/**
* Allowed Content-Transfer-Encoding parameters specified by RFC 1521
* (reduced set)
*
* @var array
*/
protected static $allowedTransferEncodings = [
'7bit',
'8bit',
'quoted-printable',
'base64',
'binary',
/*
* not implemented:
* x-token: 'X-'
*/
];
/** @var string */
protected $transferEncoding;
/** @var array */
protected $parameters = [];
/**
* @param string $headerLine
* @return static
*/
public static function fromString($headerLine)
{
[$name, $value] = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (
! in_array(
strtolower($name),
['contenttransferencoding', 'content_transfer_encoding', 'content-transfer-encoding']
)
) {
throw new Exception\InvalidArgumentException('Invalid header line for Content-Transfer-Encoding string');
}
$header = new static();
$header->setTransferEncoding($value);
return $header;
}
/**
* @return string
*/
public function getFieldName()
{
return 'Content-Transfer-Encoding';
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return $this->transferEncoding;
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
{
// Header must be always in US-ASCII
return $this;
}
/**
* @return string
*/
public function getEncoding()
{
return 'ASCII';
}
/**
* @return string
*/
public function toString()
{
return 'Content-Transfer-Encoding: ' . $this->getFieldValue();
}
/**
* Set the content transfer encoding
*
* @param string $transferEncoding
* @throws Exception\InvalidArgumentException
* @return $this
*/
public function setTransferEncoding($transferEncoding)
{
// Per RFC 1521, the value of the header is not case sensitive
$transferEncoding = strtolower($transferEncoding);
if (! in_array($transferEncoding, static::$allowedTransferEncodings)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects one of "' . implode(', ', static::$allowedTransferEncodings) . '"; received "%s"',
__METHOD__,
(string) $transferEncoding
));
}
$this->transferEncoding = $transferEncoding;
return $this;
}
/**
* Retrieve the content transfer encoding
*
* @return string
*/
public function getTransferEncoding()
{
return $this->transferEncoding;
}
}

View File

@@ -1,223 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use Laminas\Mail\Headers;
use Laminas\Mime\Mime;
use function count;
use function explode;
use function implode;
use function in_array;
use function preg_match;
use function sprintf;
use function str_replace;
use function strtolower;
use function trim;
class ContentType implements UnstructuredInterface
{
/** @var string */
protected $type;
/**
* Header encoding
*
* @var string
*/
protected $encoding = 'ASCII';
/** @var array */
protected $parameters = [];
/**
* @param string $headerLine
* @return static
*/
public static function fromString($headerLine)
{
[$name, $value] = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (! in_array(strtolower($name), ['contenttype', 'content_type', 'content-type'])) {
throw new Exception\InvalidArgumentException('Invalid header line for Content-Type string');
}
$value = str_replace(Headers::FOLDING, ' ', $value);
$parts = explode(';', $value, 2);
$header = new static();
$header->setType($parts[0]);
if (isset($parts[1])) {
$values = ListParser::parse(trim($parts[1]), [';', '=']);
$length = count($values);
for ($i = 0; $i < $length; $i += 2) {
$value = $values[$i + 1];
$value = trim($value, "'\" \t\n\r\0\x0B");
$header->addParameter($values[$i], $value);
}
}
return $header;
}
/**
* @return string
*/
public function getFieldName()
{
return 'Content-Type';
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
$prepared = $this->type;
if (empty($this->parameters)) {
return $prepared;
}
$values = [$prepared];
foreach ($this->parameters as $attribute => $value) {
if (HeaderInterface::FORMAT_ENCODED === $format && ! Mime::isPrintable($value)) {
$this->encoding = 'UTF-8';
$value = HeaderWrap::wrap($value, $this);
$this->encoding = 'ASCII';
}
$values[] = sprintf('%s="%s"', $attribute, $value);
}
return implode(';' . Headers::FOLDING, $values);
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
{
$this->encoding = $encoding;
return $this;
}
/**
* @return string
*/
public function getEncoding()
{
return $this->encoding;
}
/**
* @return string
*/
public function toString()
{
return 'Content-Type: ' . $this->getFieldValue(HeaderInterface::FORMAT_ENCODED);
}
/**
* Set the content type
*
* @param string $type
* @throws Exception\InvalidArgumentException
* @return ContentType
*/
public function setType($type)
{
if (! preg_match('/^[a-z-]+\/[a-z0-9.+-]+$/i', $type)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a value in the format "type/subtype"; received "%s"',
__METHOD__,
(string) $type
));
}
$this->type = $type;
return $this;
}
/**
* Retrieve the content type
*
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* Add a parameter pair
*
* @param string $name
* @param string $value
* @return ContentType
* @throws Exception\InvalidArgumentException For parameter names that do not follow RFC 2822.
* @throws Exception\InvalidArgumentException For parameter values that do not follow RFC 2822.
*/
public function addParameter($name, $value)
{
$name = trim(strtolower($name));
$value = (string) $value;
if (! HeaderValue::isValid($name)) {
throw new Exception\InvalidArgumentException('Invalid content-type parameter name detected');
}
if (! HeaderWrap::canBeEncoded($value)) {
throw new Exception\InvalidArgumentException(
'Parameter value must be composed of printable US-ASCII or UTF-8 characters.'
);
}
$this->parameters[$name] = $value;
return $this;
}
/**
* Get all parameters
*
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Get a parameter by name
*
* @param string $name
* @return null|string
*/
public function getParameter($name)
{
$name = strtolower($name);
if (isset($this->parameters[$name])) {
return $this->parameters[$name];
}
return null;
}
/**
* Remove a named parameter
*
* @param string $name
* @return bool
*/
public function removeParameter($name)
{
$name = strtolower($name);
if (isset($this->parameters[$name])) {
unset($this->parameters[$name]);
return true;
}
return false;
}
}

View File

@@ -1,84 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use function strtolower;
/**
* @todo Add accessors for setting date from DateTime, Laminas\Date, or a string
*/
class Date implements HeaderInterface
{
/** @var string */
protected $value;
/**
* @param string $headerLine
* @return static
*/
public static function fromString($headerLine)
{
[$name, $value] = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (strtolower($name) !== 'date') {
throw new Exception\InvalidArgumentException('Invalid header line for Date string');
}
return new static($value);
}
/**
* @param string $value
*/
public function __construct($value)
{
if (! HeaderValue::isValid($value)) {
throw new Exception\InvalidArgumentException('Invalid Date header value detected');
}
$this->value = $value;
}
/**
* @return string
*/
public function getFieldName()
{
return 'Date';
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return $this->value;
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
{
// This header must be always in US-ASCII
return $this;
}
/**
* @return string
*/
public function getEncoding()
{
return 'ASCII';
}
/**
* @return string
*/
public function toString()
{
return 'Date: ' . $this->getFieldValue();
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Mail\Header\Exception;
use Laminas\Mail\Exception;
class BadMethodCallException extends Exception\BadMethodCallException implements ExceptionInterface
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Mail\Header\Exception;
use Laminas\Mail\Exception\ExceptionInterface as MailException;
interface ExceptionInterface extends MailException
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Mail\Header\Exception;
use Laminas\Mail\Exception;
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Mail\Header\Exception;
use Laminas\Mail\Exception;
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}

View File

@@ -1,11 +0,0 @@
<?php
namespace Laminas\Mail\Header;
class From extends AbstractAddressList
{
/** @var string */
protected $fieldName = 'From';
/** @var string */
protected static $type = 'from';
}

View File

@@ -1,213 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use Laminas\Mail\Header\Exception\InvalidArgumentException;
use Laminas\Mime\Mime;
use function count;
use function explode;
use function is_string;
use function ltrim;
use function str_replace;
use function strtoupper;
use function ucwords;
class GenericHeader implements HeaderInterface, UnstructuredInterface
{
/** @var string */
protected $fieldName;
/** @var string */
protected $fieldValue = '';
/**
* Header encoding
*
* @var null|string
*/
protected $encoding;
/**
* @param string $headerLine
* @return GenericHeader
*/
public static function fromString($headerLine)
{
[$name, $value] = self::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
return new static($name, $value);
}
/**
* Splits the header line in `name` and `value` parts.
*
* @param string $headerLine
* @return string[] `name` in the first index and `value` in the second.
* @throws InvalidArgumentException If header does not match with the format ``name:value``.
*/
public static function splitHeaderLine($headerLine)
{
$parts = explode(':', $headerLine, 2);
if (count($parts) !== 2) {
throw new InvalidArgumentException('Header must match with the format "name:value"');
}
if (! HeaderName::isValid($parts[0])) {
throw new InvalidArgumentException('Invalid header name detected');
}
if (! HeaderValue::isValid($parts[1])) {
throw new InvalidArgumentException('Invalid header value detected');
}
$parts[1] = ltrim($parts[1]);
return $parts;
}
/**
* Constructor
*
* @param string $fieldName Optional
* @param null|string $fieldValue Optional
*/
public function __construct($fieldName = null, $fieldValue = null)
{
if (! $fieldName) {
throw new InvalidArgumentException('Header MUST contain a field name');
}
$this->setFieldName($fieldName);
if ($fieldValue !== null) {
$this->setFieldValue($fieldValue);
}
}
/**
* Set header name
*
* @param string $fieldName
* @return GenericHeader
* @throws Exception\InvalidArgumentException;
*/
public function setFieldName($fieldName)
{
if (! is_string($fieldName) || empty($fieldName)) {
throw new InvalidArgumentException('Header name must be a string');
}
// Pre-filter to normalize valid characters, change underscore to dash
$fieldName = str_replace(' ', '-', ucwords(str_replace(['_', '-'], ' ', $fieldName)));
if (! HeaderName::isValid($fieldName)) {
throw new InvalidArgumentException(
'Header name must be composed of printable US-ASCII characters, except colon.'
);
}
$this->fieldName = $fieldName;
return $this;
}
/**
* @return string
*/
public function getFieldName()
{
return $this->fieldName;
}
/**
* Set header value
*
* @param string $fieldValue
* @return GenericHeader
* @throws Exception\InvalidArgumentException;
*/
public function setFieldValue($fieldValue)
{
$fieldValue = (string) $fieldValue;
if (! HeaderWrap::canBeEncoded($fieldValue)) {
throw new InvalidArgumentException(
'Header value must be composed of printable US-ASCII characters and valid folding sequences.'
);
}
$this->fieldValue = $fieldValue;
$this->encoding = null;
return $this;
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
if (HeaderInterface::FORMAT_ENCODED === $format) {
return HeaderWrap::wrap($this->fieldValue, $this);
}
return $this->fieldValue;
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
{
if ($encoding === $this->encoding) {
return $this;
}
if ($encoding === null) {
$this->encoding = null;
return $this;
}
$encoding = strtoupper($encoding);
if ($encoding === 'UTF-8') {
$this->encoding = $encoding;
return $this;
}
if ($encoding === 'ASCII' && Mime::isPrintable($this->fieldValue)) {
$this->encoding = $encoding;
return $this;
}
$this->encoding = null;
return $this;
}
/**
* @return string
*/
public function getEncoding()
{
if (! $this->encoding) {
$this->encoding = Mime::isPrintable($this->fieldValue) ? 'ASCII' : 'UTF-8';
}
return $this->encoding;
}
/**
* @return string
*/
public function toString()
{
$name = $this->getFieldName();
if (empty($name)) {
throw new Exception\RuntimeException('Header name is not set, use setFieldName()');
}
$value = $this->getFieldValue(HeaderInterface::FORMAT_ENCODED);
return $name . ': ' . $value;
}
}

View File

@@ -1,57 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use function explode;
use function implode;
use function strpos;
/**
* Generic class for Headers with multiple occurs in the same message
*/
class GenericMultiHeader extends GenericHeader implements MultipleHeadersInterface
{
/**
* @param string $headerLine
* @return array|GenericHeader|GenericMultiHeader|static
*/
public static function fromString($headerLine)
{
[$fieldName, $fieldValue] = GenericHeader::splitHeaderLine($headerLine);
$fieldValue = HeaderWrap::mimeDecodeValue($fieldValue);
if (strpos($fieldValue, ',')) {
$headers = [];
foreach (explode(',', $fieldValue) as $multiValue) {
$headers[] = new static($fieldName, $multiValue);
}
return $headers;
}
return new static($fieldName, $fieldValue);
}
/**
* Cast multiple header objects to a single string header
*
* @param array $headers
* @throws Exception\InvalidArgumentException
* @return string
*/
public function toStringMultipleHeaders(array $headers)
{
$name = $this->getFieldName();
$values = [$this->getFieldValue(HeaderInterface::FORMAT_ENCODED)];
foreach ($headers as $header) {
if (! $header instanceof static) {
throw new Exception\InvalidArgumentException(
'This method toStringMultipleHeaders was expecting an array of headers of the same type'
);
}
$values[] = $header->getFieldValue(HeaderInterface::FORMAT_ENCODED);
}
return $name . ': ' . implode(',', $values);
}
}

View File

@@ -1,70 +0,0 @@
<?php
namespace Laminas\Mail\Header;
interface HeaderInterface
{
/**
* Format value in Mime-Encoding (Quoted-Printable). Result is valid US-ASCII string
*
* @var bool
*/
public const FORMAT_ENCODED = true;
/**
* Return value in internal encoding which is usually UTF-8
*
* @var bool
*/
public const FORMAT_RAW = false;
/**
* Factory to generate a header object from a string
*
* @see http://tools.ietf.org/html/rfc2822#section-2.2
*
* @param string $headerLine
* @return static
* @throws Exception\InvalidArgumentException If the header does not match with RFC 2822 definition.
*/
public static function fromString($headerLine);
/**
* Retrieve header name
*
* @return string
*/
public function getFieldName();
/**
* Retrieve header value
*
* @param HeaderInterface::FORMAT_* $format Return the value in Mime::Encoded or in Raw format
* @return string
*/
public function getFieldValue($format = self::FORMAT_RAW);
/**
* Set header encoding
*
* @param string $encoding
* @return $this
*/
public function setEncoding($encoding);
/**
* Get header encoding
*
* @return string
*/
public function getEncoding();
/**
* Cast to string
*
* Returns in form of "NAME: VALUE"
*
* @return string
*/
public function toString();
}

View File

@@ -1,41 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use Laminas\Loader\PluginClassLoader;
/**
* Plugin Class Loader implementation for HTTP headers
*/
class HeaderLoader extends PluginClassLoader
{
/** @var array Pre-aliased Header plugins */
protected $plugins = [
'bcc' => Bcc::class,
'cc' => Cc::class,
'contentdisposition' => ContentDisposition::class,
'content_disposition' => ContentDisposition::class,
'content-disposition' => ContentDisposition::class,
'contenttype' => ContentType::class,
'content_type' => ContentType::class,
'content-type' => ContentType::class,
'contenttransferencoding' => ContentTransferEncoding::class,
'content_transfer_encoding' => ContentTransferEncoding::class,
'content-transfer-encoding' => ContentTransferEncoding::class,
'date' => Date::class,
'from' => From::class,
'in-reply-to' => InReplyTo::class,
'message-id' => MessageId::class,
'mimeversion' => MimeVersion::class,
'mime_version' => MimeVersion::class,
'mime-version' => MimeVersion::class,
'received' => Received::class,
'references' => References::class,
'replyto' => ReplyTo::class,
'reply_to' => ReplyTo::class,
'reply-to' => ReplyTo::class,
'sender' => Sender::class,
'subject' => Subject::class,
'to' => To::class,
];
}

View File

@@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Mail\Header;
use function strtolower;
/**
* Plugin Class Loader implementation for HTTP headers
*/
final class HeaderLocator implements HeaderLocatorInterface
{
/** @var array Pre-aliased Header plugins */
private array $plugins = [
'bcc' => Bcc::class,
'cc' => Cc::class,
'contentdisposition' => ContentDisposition::class,
'content_disposition' => ContentDisposition::class,
'content-disposition' => ContentDisposition::class,
'contenttype' => ContentType::class,
'content_type' => ContentType::class,
'content-type' => ContentType::class,
'contenttransferencoding' => ContentTransferEncoding::class,
'content_transfer_encoding' => ContentTransferEncoding::class,
'content-transfer-encoding' => ContentTransferEncoding::class,
'date' => Date::class,
'from' => From::class,
'in-reply-to' => InReplyTo::class,
'message-id' => MessageId::class,
'mimeversion' => MimeVersion::class,
'mime_version' => MimeVersion::class,
'mime-version' => MimeVersion::class,
'received' => Received::class,
'references' => References::class,
'replyto' => ReplyTo::class,
'reply_to' => ReplyTo::class,
'reply-to' => ReplyTo::class,
'sender' => Sender::class,
'subject' => Subject::class,
'to' => To::class,
];
public function get(string $name, ?string $default = null): ?string
{
$name = $this->normalizeName($name);
return $this->plugins[$name] ?? $default;
}
public function has(string $name): bool
{
return isset($this->plugins[$this->normalizeName($name)]);
}
public function add(string $name, string $class): void
{
$this->plugins[$this->normalizeName($name)] = $class;
}
public function remove(string $name): void
{
unset($this->plugins[$this->normalizeName($name)]);
}
private function normalizeName(string $name): string
{
return strtolower($name);
}
}

View File

@@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Mail\Header;
/**
* Interface detailing how to resolve header names to classes.
*/
interface HeaderLocatorInterface
{
/**
* @param class-string<HeaderInterface>|null $default
* @return class-string<HeaderInterface>|null
*/
public function get(string $name, ?string $default = null): ?string;
public function has(string $name): bool;
public function add(string $name, string $class): void;
public function remove(string $name): void;
}

View File

@@ -1,71 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use function ord;
use function strlen;
final class HeaderName
{
/**
* No public constructor.
*/
private function __construct()
{
}
/**
* Filter the header name according to RFC 2822
*
* @see http://www.rfc-base.org/txt/rfc-2822.txt (section 2.2)
*
* @param string $name
* @return string
*/
public static function filter($name)
{
$result = '';
$tot = strlen($name);
for ($i = 0; $i < $tot; $i += 1) {
$ord = ord($name[$i]);
if ($ord > 32 && $ord < 127 && $ord !== 58) {
$result .= $name[$i];
}
}
return $result;
}
/**
* Determine if the header name contains any invalid characters.
*
* @param string $name
* @return bool
*/
public static function isValid($name)
{
$tot = strlen($name);
for ($i = 0; $i < $tot; $i += 1) {
$ord = ord($name[$i]);
if ($ord < 33 || $ord > 126 || $ord === 58) {
return false;
}
}
return true;
}
/**
* Assert that the header name is valid.
*
* Raises an exception if invalid.
*
* @param string $name
* @throws Exception\RuntimeException
* @return void
*/
public static function assertValid($name)
{
if (! self::isValid($name)) {
throw new Exception\RuntimeException('Invalid header name detected');
}
}
}

View File

@@ -1,116 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use function in_array;
use function ord;
use function strlen;
final class HeaderValue
{
/**
* No public constructor.
*/
private function __construct()
{
}
/**
* Filter the header value according to RFC 2822
*
* @see http://www.rfc-base.org/txt/rfc-2822.txt (section 2.2)
*
* @param string $value
* @return string
*/
public static function filter($value)
{
$result = '';
$total = strlen($value);
// Filter for CR and LF characters, leaving CRLF + WSP sequences for
// Long Header Fields (section 2.2.3 of RFC 2822)
for ($i = 0; $i < $total; $i += 1) {
$ord = ord($value[$i]);
if ($ord === 10 || $ord > 127) {
continue;
}
if ($ord === 13) {
if ($i + 2 >= $total) {
continue;
}
$lf = ord($value[$i + 1]);
$sp = ord($value[$i + 2]);
if ($lf !== 10 || $sp !== 32) {
continue;
}
$result .= "\r\n ";
$i += 2;
continue;
}
$result .= $value[$i];
}
return $result;
}
/**
* Determine if the header value contains any invalid characters.
*
* @see http://www.rfc-base.org/txt/rfc-2822.txt (section 2.2)
*
* @param string $value
* @return bool
*/
public static function isValid($value)
{
$total = strlen($value);
for ($i = 0; $i < $total; $i += 1) {
$ord = ord($value[$i]);
// bare LF means we aren't valid
if ($ord === 10 || $ord > 127) {
return false;
}
if ($ord === 13) {
if ($i + 2 >= $total) {
return false;
}
$lf = ord($value[$i + 1]);
$sp = ord($value[$i + 2]);
if ($lf !== 10 || ! in_array($sp, [9, 32], true)) {
return false;
}
// skip over the LF following this
$i += 2;
}
}
return true;
}
/**
* Assert that the header value is valid.
*
* Raises an exception if invalid.
*
* @param string $value
* @throws Exception\RuntimeException
* @return void
*/
public static function assertValid($value)
{
if (! self::isValid($value)) {
throw new Exception\RuntimeException('Invalid header value detected');
}
}
}

View File

@@ -1,185 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use Laminas\Mail\Headers;
use Laminas\Mime\Mime;
use function array_reduce;
use function explode;
use function extension_loaded;
use function iconv_mime_decode;
use function iconv_mime_encode;
use function implode;
use function mb_decode_mimeheader;
use function str_contains;
use function str_pad;
use function str_starts_with;
use function strlen;
use function strpos;
use function substr;
use function wordwrap;
use const ICONV_MIME_DECODE_CONTINUE_ON_ERROR;
/**
* Utility class used for creating wrapped or MIME-encoded versions of header
* values.
*/
// phpcs:ignore WebimpressCodingStandard.NamingConventions.AbstractClass.Prefix
abstract class HeaderWrap
{
/**
* Wrap a long header line
*
* @param string $value
* @return string
*/
public static function wrap($value, HeaderInterface $header)
{
if ($header instanceof UnstructuredInterface) {
return static::wrapUnstructuredHeader($value, $header);
} elseif ($header instanceof StructuredInterface) {
return static::wrapStructuredHeader($value, $header);
}
return $value;
}
/**
* Wrap an unstructured header line
*
* Wrap at 78 characters or before, based on whitespace.
*
* @param string $value
* @return string
*/
protected static function wrapUnstructuredHeader($value, HeaderInterface $header)
{
$headerNameColonSize = strlen($header->getFieldName() . ': ');
$encoding = $header->getEncoding();
if ($encoding == 'ASCII') {
/*
* Before folding the header line, it is necessary to calculate the length of the
* entire header (including the name and colon). We need to put a stub at the
* beginning of the value so that the folding is performed correctly.
*/
$headerLine = str_pad('0', $headerNameColonSize, '0') . $value;
$foldedHeaderLine = wordwrap($headerLine, 78, Headers::FOLDING);
// Remove the stub and return the header folded value.
return substr($foldedHeaderLine, $headerNameColonSize);
}
return static::mimeEncodeValue($value, $encoding, 78, $headerNameColonSize);
}
/**
* Wrap a structured header line
*
* @param string $value
* @return string
*/
protected static function wrapStructuredHeader($value, StructuredInterface $header)
{
$delimiter = $header->getDelimiter();
$length = strlen($value);
$lines = [];
$temp = '';
for ($i = 0; $i < $length; $i++) {
$temp .= $value[$i];
if ($value[$i] == $delimiter) {
$lines[] = $temp;
$temp = '';
}
}
return implode(Headers::FOLDING, $lines);
}
/**
* MIME-encode a value
*
* Performs quoted-printable encoding on a value, setting maximum
* line-length to 998.
*
* @param string $value
* @param string $encoding
* @param int $lineLength Maximum line-length, by default 998
* @param positive-int|0 $firstLineGapSize When folding a line, it is necessary to calculate
* the length of the entire line (together with the
* header name). Therefore, you can specify the header
* name and colon length in this argument to fold the
* string properly.
* @return string Returns the mime encode value without the last line ending
*/
public static function mimeEncodeValue($value, $encoding, $lineLength = 998, $firstLineGapSize = 0)
{
return Mime::encodeQuotedPrintableHeader($value, $encoding, $lineLength, Headers::EOL, $firstLineGapSize);
}
/**
* MIME-decode a value
*
* Performs quoted-printable decoding on a value.
*
* @param string $value
* @return string Returns the mime encode value without the last line ending
*/
public static function mimeDecodeValue($value)
{
// unfold first, because iconv_mime_decode is discarding "\n" with no apparent reason
// making the resulting value no longer valid.
// see https://tools.ietf.org/html/rfc2822#section-2.2.3 about unfolding
$parts = explode(Headers::FOLDING, $value);
$value = implode(' ', $parts);
$decodedValue = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8');
// imap (unlike iconv) can handle multibyte headers which are splitted across multiple line
if (self::isNotDecoded($value, $decodedValue) && extension_loaded('imap')) {
return array_reduce(
mb_decode_mimeheader($value),
static fn($accumulator, $headerPart) => $accumulator . $headerPart->text,
''
);
}
return $decodedValue;
}
private static function isNotDecoded(string $originalValue, string $value): bool
{
return str_starts_with($value, '=?')
&& strlen($value) - 2 === strpos($value, '?=')
&& str_contains($originalValue, $value);
}
/**
* Test if is possible apply MIME-encoding
*
* @param string $value
* @return bool
*/
public static function canBeEncoded($value)
{
// avoid any wrapping by specifying line length long enough
// "test" -> 4
// "x-test: =?ISO-8859-1?B?dGVzdA==?=" -> 33
// 8 +2 +3 +3 -> 16
$charset = 'UTF-8';
$lineLength = strlen($value) * 4 + strlen($charset) + 16;
$preferences = [
'scheme' => 'Q',
'input-charset' => $charset,
'output-charset' => $charset,
'line-length' => $lineLength,
];
$encoded = iconv_mime_encode('x-test', $value, $preferences);
return false !== $encoded;
}
}

View File

@@ -1,138 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use Laminas\Mail\Headers;
use function array_map;
use function explode;
use function implode;
use function preg_match;
use function sprintf;
use function strtolower;
use function trim;
/**
* @see https://tools.ietf.org/html/rfc5322#section-3.6.4
*/
// phpcs:ignore WebimpressCodingStandard.NamingConventions.AbstractClass.Prefix
abstract class IdentificationField implements HeaderInterface
{
/** @var string lower case field name */
protected static $type;
/** @var string[] */
protected $messageIds;
/** @var string */
protected $fieldName;
/**
* @param string $headerLine
* @return static
*/
public static function fromString($headerLine)
{
[$name, $value] = GenericHeader::splitHeaderLine($headerLine);
if (strtolower($name) !== static::$type) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid header line for "%s" string',
self::class
));
}
$value = HeaderWrap::mimeDecodeValue($value);
$messageIds = array_map(
[self::class, "trimMessageId"],
explode(" ", $value)
);
$header = new static();
$header->setIds($messageIds);
return $header;
}
/**
* @param string $id
* @return string
*/
private static function trimMessageId($id)
{
return trim($id, "\t\n\r\0\x0B<>");
}
/**
* @return string
*/
public function getFieldName()
{
return $this->fieldName;
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return implode(Headers::FOLDING, array_map(static fn($id) => sprintf('<%s>', $id), $this->messageIds));
}
/**
* @param string $encoding Ignored; headers of this type MUST always be in
* ASCII.
* @return static This method is a no-op, and implements a fluent interface.
*/
public function setEncoding($encoding)
{
return $this;
}
/**
* @return string Always returns ASCII
*/
public function getEncoding()
{
return 'ASCII';
}
/**
* @return string
*/
public function toString()
{
return sprintf('%s: %s', $this->getFieldName(), $this->getFieldValue());
}
/**
* Set the message ids
*
* @param string[] $ids
* @return static This method implements a fluent interface.
*/
public function setIds($ids)
{
foreach ($ids as $id) {
if (
! HeaderValue::isValid($id)
|| preg_match("/[\r\n]/", $id)
) {
throw new Exception\InvalidArgumentException('Invalid ID detected');
}
}
$this->messageIds = array_map([self::class, "trimMessageId"], $ids);
return $this;
}
/**
* Retrieve the message ids
*
* @return string[]
*/
public function getIds()
{
return $this->messageIds;
}
}

View File

@@ -1,11 +0,0 @@
<?php
namespace Laminas\Mail\Header;
class InReplyTo extends IdentificationField
{
/** @var string */
protected $fieldName = 'In-Reply-To';
/** @var string */
protected static $type = 'in-reply-to';
}

View File

@@ -1,94 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use function in_array;
use function strlen;
/**
* @internal
*/
class ListParser
{
public const CHAR_QUOTES = ['\'', '"'];
public const CHAR_DELIMS = [',', ';'];
public const CHAR_ESCAPE = '\\';
/**
* @param string $value
* @param array $delims Delimiters allowed between values; parser will
* split on these, as long as they are not within quotes. Defaults
* to ListParser::CHAR_DELIMS.
* @return array
*/
public static function parse($value, array $delims = self::CHAR_DELIMS)
{
$values = [];
$length = strlen($value);
$currentValue = '';
$inEscape = false;
$inQuote = false;
$currentQuoteDelim = null;
for ($i = 0; $i < $length; $i += 1) {
$char = $value[$i];
// If we are in an escape sequence, append the character and continue.
if ($inEscape) {
$currentValue .= $char;
$inEscape = false;
continue;
}
// If we are not in a quoted string, and have a delimiter, append
// the current value to the list, and reset the current value.
if (in_array($char, $delims, true) && ! $inQuote) {
$values [] = $currentValue;
$currentValue = '';
continue;
}
// Append the character to the current value
$currentValue .= $char;
// Escape sequence discovered.
if (self::CHAR_ESCAPE === $char) {
$inEscape = true;
continue;
}
// If the character is not a quote character, we are done
// processing it.
if (! in_array($char, self::CHAR_QUOTES)) {
continue;
}
// If the character matches a previously matched quote delimiter,
// we reset our quote status and the currently opened quote
// delimiter.
if ($char === $currentQuoteDelim) {
$inQuote = false;
$currentQuoteDelim = null;
continue;
}
// If already in quote and the character does not match the previously
// matched quote delimiter, we're done here.
if ($inQuote) {
continue;
}
// Otherwise, we're starting a quoted string.
$inQuote = true;
$currentQuoteDelim = $char;
}
// If we reached the end of the string and still have a current value,
// append it to the list (no delimiter was reached).
if ('' !== $currentValue) {
$values [] = $currentValue;
}
return $values;
}
}

View File

@@ -1,142 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use function getmypid;
use function mt_rand;
use function php_uname;
use function preg_match;
use function sha1;
use function sprintf;
use function strtolower;
use function time;
use function trim;
class MessageId implements HeaderInterface
{
/** @var string */
protected $messageId;
/**
* @param string $headerLine
* @return static
*/
public static function fromString($headerLine)
{
[$name, $value] = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (strtolower($name) !== 'message-id') {
throw new Exception\InvalidArgumentException('Invalid header line for Message-ID string');
}
$header = new static();
$header->setId($value);
return $header;
}
/**
* @return string
*/
public function getFieldName()
{
return 'Message-ID';
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return $this->messageId;
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
{
// This header must be always in US-ASCII
return $this;
}
/**
* @return string
*/
public function getEncoding()
{
return 'ASCII';
}
/**
* @return string
*/
public function toString()
{
return 'Message-ID: ' . $this->getFieldValue();
}
/**
* Set the message id
*
* @param string|null $id
* @return MessageId
*/
public function setId($id = null)
{
if ($id === null) {
$id = $this->createMessageId();
} else {
$id = trim($id, '<>');
}
if (
! HeaderValue::isValid($id)
|| preg_match("/[\r\n]/", $id)
) {
throw new Exception\InvalidArgumentException('Invalid ID detected');
}
$this->messageId = sprintf('<%s>', $id);
return $this;
}
/**
* Retrieve the message id
*
* @return string
*/
public function getId()
{
return $this->messageId;
}
/**
* Creates the Message-ID
*
* @return string
*/
public function createMessageId()
{
$time = time();
if (isset($_SERVER['REMOTE_ADDR'])) {
$user = $_SERVER['REMOTE_ADDR'];
} else {
$user = getmypid();
}
$rand = mt_rand();
if (isset($_SERVER["SERVER_NAME"])) {
$hostName = $_SERVER["SERVER_NAME"];
} else {
$hostName = php_uname('n');
}
return sha1($time . $user . $rand) . '@' . $hostName;
}
}

View File

@@ -1,103 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use function in_array;
use function preg_match;
use function strtolower;
class MimeVersion implements HeaderInterface
{
/** @var string Version string */
protected $version = '1.0';
/**
* @param string $headerLine
* @return static
*/
public static function fromString($headerLine)
{
[$name, $value] = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (! in_array(strtolower($name), ['mimeversion', 'mime_version', 'mime-version'])) {
throw new Exception\InvalidArgumentException('Invalid header line for MIME-Version string');
}
// Check for version, and set if found
$header = new static();
if (preg_match('/^(?P<version>\d+\.\d+)$/', $value, $matches)) {
$header->setVersion($matches['version']);
}
return $header;
}
/**
* @return string
*/
public function getFieldName()
{
return 'MIME-Version';
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return $this->version;
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
{
// This header must be always in US-ASCII
return $this;
}
/**
* @return string
*/
public function getEncoding()
{
return 'ASCII';
}
/**
* @return string
*/
public function toString()
{
return 'MIME-Version: ' . $this->getFieldValue();
}
/**
* Set the version string used in this header
*
* @param string $version
* @return MimeVersion
*/
public function setVersion($version)
{
if (! preg_match('/^[1-9]\d*\.\d+$/', $version)) {
throw new Exception\InvalidArgumentException('Invalid MIME-Version value detected');
}
$this->version = $version;
return $this;
}
/**
* Retrieve the version string for this header
*
* @return string
*/
public function getVersion()
{
return $this->version;
}
}

View File

@@ -1,8 +0,0 @@
<?php
namespace Laminas\Mail\Header;
interface MultipleHeadersInterface extends HeaderInterface
{
public function toStringMultipleHeaders(array $headers);
}

View File

@@ -1,108 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use Laminas\Mail\Headers;
use function implode;
use function strtolower;
/**
* @todo Allow setting date from DateTime, Laminas\Date, or string
*/
class Received implements HeaderInterface, MultipleHeadersInterface
{
/** @var string */
protected $value;
/**
* @param string $headerLine
* @return static
*/
public static function fromString($headerLine)
{
[$name, $value] = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (strtolower($name) !== 'received') {
throw new Exception\InvalidArgumentException('Invalid header line for Received string');
}
return new static($value);
}
/**
* @param string $value
*/
public function __construct($value = '')
{
if (! HeaderValue::isValid($value)) {
throw new Exception\InvalidArgumentException('Invalid Received value provided');
}
$this->value = $value;
}
/**
* @return string
*/
public function getFieldName()
{
return 'Received';
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return $this->value;
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
{
// This header must be always in US-ASCII
return $this;
}
/**
* @return string
*/
public function getEncoding()
{
return 'ASCII';
}
/**
* @return string
*/
public function toString()
{
return 'Received: ' . $this->getFieldValue();
}
/**
* Serialize collection of Received headers to string
*
* @param array $headers
* @throws Exception\RuntimeException
* @return string
*/
public function toStringMultipleHeaders(array $headers)
{
$strings = [$this->toString()];
foreach ($headers as $header) {
if (! $header instanceof self) {
throw new Exception\RuntimeException(
'The Received multiple header implementation can only accept an array of Received headers'
);
}
$strings[] = $header->toString();
}
return implode(Headers::EOL, $strings);
}
}

View File

@@ -1,11 +0,0 @@
<?php
namespace Laminas\Mail\Header;
class References extends IdentificationField
{
/** @var string */
protected $fieldName = 'References';
/** @var string */
protected static $type = 'references';
}

View File

@@ -1,13 +0,0 @@
<?php
namespace Laminas\Mail\Header;
class ReplyTo extends AbstractAddressList
{
/** @var string */
protected $fieldName = 'Reply-To';
/** @var string */
protected static $type = 'reply-to';
/** @var string[] */
protected static $typeAliases = ['replyto', 'reply_to'];
}

View File

@@ -1,175 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use Laminas\Mail;
use Laminas\Mail\Address\AddressInterface;
use Laminas\Mime\Mime;
use function gettype;
use function is_object;
use function is_string;
use function preg_match;
use function sprintf;
use function strtolower;
use function trim;
/**
* Sender header class methods.
*
* @see https://tools.ietf.org/html/rfc2822 RFC 2822
* @see https://tools.ietf.org/html/rfc2047 RFC 2047
*/
class Sender implements HeaderInterface
{
/** @var AddressInterface */
protected $address;
/**
* Header encoding
*
* @var null|string
*/
protected $encoding;
/**
* @param string $headerLine
* @return static
*/
public static function fromString($headerLine)
{
[$name, $value] = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (strtolower($name) !== 'sender') {
throw new Exception\InvalidArgumentException('Invalid header name for Sender string');
}
$header = new static();
/**
* matches the header value so that the email must be enclosed by < > when a name is present
* 'name' and 'email' capture groups correspond respectively to 'display-name' and 'addr-spec' in the ABNF
*
* @see https://tools.ietf.org/html/rfc5322#section-3.4
*/
$hasMatches = preg_match(
'/^(?:(?P<name>.+)\s)?(?(name)<|<?)(?P<email>[^\s]+?)(?(name)>|>?)$/',
$value,
$matches
);
if ($hasMatches !== 1) {
throw new Exception\InvalidArgumentException('Invalid header value for Sender string');
}
$senderName = trim($matches['name']);
if (empty($senderName)) {
$senderName = null;
}
$header->setAddress($matches['email'], $senderName);
return $header;
}
/**
* @return string
*/
public function getFieldName()
{
return 'Sender';
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
if (! $this->address instanceof Mail\Address\AddressInterface) {
return '';
}
$email = sprintf('<%s>', $this->address->getEmail());
$name = $this->address->getName();
if (! empty($name)) {
if ($format == HeaderInterface::FORMAT_ENCODED) {
$encoding = $this->getEncoding();
if ('ASCII' !== $encoding) {
$name = HeaderWrap::mimeEncodeValue($name, $encoding);
}
}
$email = sprintf('%s %s', $name, $email);
}
return $email;
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
{
$this->encoding = $encoding;
return $this;
}
/**
* @return string
*/
public function getEncoding()
{
if (! $this->encoding) {
$this->encoding = Mime::isPrintable($this->getFieldValue(HeaderInterface::FORMAT_RAW))
? 'ASCII'
: 'UTF-8';
}
return $this->encoding;
}
/**
* @return string
*/
public function toString()
{
return 'Sender: ' . $this->getFieldValue(HeaderInterface::FORMAT_ENCODED);
}
/**
* Set the address used in this header
*
* @param string|AddressInterface $emailOrAddress
* @param null|string $name
* @throws Exception\InvalidArgumentException
* @return Sender
*/
public function setAddress($emailOrAddress, $name = null)
{
if (is_string($emailOrAddress)) {
$emailOrAddress = new Mail\Address($emailOrAddress, $name);
} elseif (! $emailOrAddress instanceof Mail\Address\AddressInterface) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a string or AddressInterface object; received "%s"',
__METHOD__,
is_object($emailOrAddress) ? $emailOrAddress::class : gettype($emailOrAddress)
));
}
$this->address = $emailOrAddress;
return $this;
}
/**
* Retrieve the internal address from this header
*
* @return AddressInterface|null
*/
public function getAddress()
{
return $this->address;
}
}

View File

@@ -1,13 +0,0 @@
<?php
namespace Laminas\Mail\Header;
interface StructuredInterface extends HeaderInterface
{
/**
* Return the delimiter at which a header line should be wrapped
*
* @return string
*/
public function getDelimiter();
}

View File

@@ -1,138 +0,0 @@
<?php
namespace Laminas\Mail\Header;
use Laminas\Mime\Mime;
use function strtolower;
use function strtoupper;
/**
* Subject header class methods.
*
* @see https://tools.ietf.org/html/rfc2822 RFC 2822
* @see https://tools.ietf.org/html/rfc2047 RFC 2047
*/
class Subject implements UnstructuredInterface
{
/** @var string */
protected $subject = '';
/**
* Header encoding
*
* @var null|string
*/
protected $encoding;
/**
* @param string $headerLine
* @return static
*/
public static function fromString($headerLine)
{
[$name, $value] = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (strtolower($name) !== 'subject') {
throw new Exception\InvalidArgumentException('Invalid header line for Subject string');
}
$header = new static();
$header->setSubject($value);
return $header;
}
/**
* @return string
*/
public function getFieldName()
{
return 'Subject';
}
/**
* @inheritDoc
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
if (HeaderInterface::FORMAT_ENCODED === $format) {
return HeaderWrap::wrap($this->subject, $this);
}
return $this->subject;
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
{
if ($encoding === $this->encoding) {
return $this;
}
if ($encoding === null) {
$this->encoding = null;
return $this;
}
$encoding = strtoupper($encoding);
if ($encoding === 'UTF-8') {
$this->encoding = $encoding;
return $this;
}
if ($encoding === 'ASCII' && Mime::isPrintable($this->subject)) {
$this->encoding = $encoding;
return $this;
}
$this->encoding = null;
return $this;
}
/**
* @return string
*/
public function getEncoding()
{
if (! $this->encoding) {
$this->encoding = Mime::isPrintable($this->subject) ? 'ASCII' : 'UTF-8';
}
return $this->encoding;
}
/**
* @param string $subject
* @return self
*/
public function setSubject($subject)
{
$subject = (string) $subject;
if (! HeaderWrap::canBeEncoded($subject)) {
throw new Exception\InvalidArgumentException(
'Subject value must be composed of printable US-ASCII or UTF-8 characters.'
);
}
$this->subject = $subject;
$this->encoding = null;
return $this;
}
/**
* @return string
*/
public function toString()
{
return 'Subject: ' . $this->getFieldValue(HeaderInterface::FORMAT_ENCODED);
}
}

View File

@@ -1,11 +0,0 @@
<?php
namespace Laminas\Mail\Header;
class To extends AbstractAddressList
{
/** @var string */
protected $fieldName = 'To';
/** @var string */
protected static $type = 'to';
}

View File

@@ -1,10 +0,0 @@
<?php
namespace Laminas\Mail\Header;
/**
* Marker interface for unstructured headers.
*/
interface UnstructuredInterface extends HeaderInterface
{
}

View File

@@ -1,632 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Mail;
use ArrayIterator;
use Countable;
use Iterator;
use Laminas\Loader\PluginClassLocator;
use Laminas\Mail\Header\GenericHeader;
use Laminas\Mail\Header\HeaderInterface;
use Laminas\Mail\Header\HeaderLocatorInterface;
use ReturnTypeWillChange;
use Traversable;
use function array_keys;
use function array_shift;
use function assert;
use function count;
use function current;
use function explode;
use function gettype;
use function in_array;
use function is_array;
use function is_int;
use function is_object;
use function is_string;
use function key;
use function next;
use function preg_match;
use function reset;
use function sprintf;
use function str_replace;
use function strtolower;
use function trigger_error;
use function trim;
use const E_USER_DEPRECATED;
/**
* Basic mail headers collection functionality
*
* Handles aggregation of headers
*
* @implements Iterator<int, HeaderInterface>
*/
class Headers implements Countable, Iterator
{
/** @var string End of Line for fields */
public const EOL = "\r\n";
/** @var string Start of Line when folding */
public const FOLDING = "\r\n ";
private ?HeaderLocatorInterface $headerLocator = null;
/**
* @todo Remove for 3.0.0.
* @var null|PluginClassLocator
*/
protected $pluginClassLoader;
/** @var list<string> key names for $headers array */
protected $headersKeys = [];
/** @var list<HeaderInterface> instances */
protected $headers = [];
/**
* Header encoding; defaults to ASCII
*
* @var string
*/
protected $encoding = 'ASCII';
/**
* Populates headers from string representation
*
* Parses a string for headers, and aggregates them, in order, in the
* current instance, primarily as strings until they are needed (they
* will be lazy loaded)
*
* @param string $string
* @param string $eol EOL string; defaults to {@link EOL}
* @return Headers
* @throws Exception\RuntimeException
*/
public static function fromString($string, $eol = self::EOL)
{
$headers = new static();
$currentLine = '';
$emptyLine = 0;
// iterate the header lines, some might be continuations
$lines = explode($eol, $string);
$total = count($lines);
for ($i = 0; $i < $total; $i += 1) {
$line = $lines[$i];
if ($line === "") {
// Empty line indicates end of headers
// EXCEPT if there are more lines, in which case, there's a possible error condition
$emptyLine += 1;
if ($emptyLine > 2) {
throw new Exception\RuntimeException('Malformed header detected');
}
continue;
} elseif (preg_match('/^\s*$/', $line)) {
// skip empty continuation line
continue;
}
if ($emptyLine > 1) {
throw new Exception\RuntimeException('Malformed header detected');
}
// check if a header name is present
if (preg_match('/^[\x21-\x39\x3B-\x7E]+:.*$/', $line)) {
if ($currentLine) {
// a header name was present, then store the current complete line
$headers->addHeaderLine($currentLine);
}
$currentLine = trim($line);
continue;
}
// continuation: append to current line
// recover the whitespace that break the line (unfolding, rfc2822#section-2.2.3)
if (preg_match('/^\s+.*$/', $line)) {
$currentLine .= ' ' . trim($line);
continue;
}
// Line does not match header format!
throw new Exception\RuntimeException(sprintf(
'Line "%s" does not match header format!',
$line
));
}
if ($currentLine) {
$headers->addHeaderLine($currentLine);
}
return $headers;
}
/**
* Set an alternate PluginClassLocator implementation for loading header classes.
*
* @deprecated since 2.12.0
*
* @todo Remove for version 3.0.0
* @return $this
*/
public function setPluginClassLoader(PluginClassLocator $pluginClassLoader)
{
// Silenced; can be caught in custom error handlers.
@trigger_error(sprintf(
'Since laminas/laminas-mail 2.12.0: Usage of %s is deprecated; use %s::setHeaderLocator() instead',
__METHOD__,
self::class
), E_USER_DEPRECATED);
$this->pluginClassLoader = $pluginClassLoader;
return $this;
}
/**
* Return a PluginClassLocator instance for customizing headers.
*
* Lazyloads a Header\HeaderLoader if necessary.
*
* @deprecated since 2.12.0
*
* @todo Remove for version 3.0.0
* @return PluginClassLocator
*/
public function getPluginClassLoader()
{
// Silenced; can be caught in custom error handlers.
@trigger_error(sprintf(
'Since laminas/laminas-mail 2.12.0: Usage of %s is deprecated; use %s::getHeaderLocator() instead',
__METHOD__,
self::class
), E_USER_DEPRECATED);
if (! $this->pluginClassLoader) {
$this->pluginClassLoader = new Header\HeaderLoader();
}
return $this->pluginClassLoader;
}
/**
* Retrieve the header class locator for customizing headers.
*
* Lazyloads a Header\HeaderLocator instance if necessary.
*/
public function getHeaderLocator(): HeaderLocatorInterface
{
if (! $this->headerLocator) {
$this->setHeaderLocator(new Header\HeaderLocator());
}
assert($this->headerLocator instanceof HeaderLocatorInterface);
return $this->headerLocator;
}
/**
* @todo Return self when we update to 7.4 or later as minimum PHP version.
* @return $this
*/
public function setHeaderLocator(HeaderLocatorInterface $headerLocator)
{
$this->headerLocator = $headerLocator;
return $this;
}
/**
* Set the header encoding
*
* @param string $encoding
* @return Headers
*/
public function setEncoding($encoding)
{
$this->encoding = $encoding;
foreach ($this as $header) {
$header->setEncoding($encoding);
}
return $this;
}
/**
* Get the header encoding
*
* @return string
*/
public function getEncoding()
{
return $this->encoding;
}
/**
* Add many headers at once
*
* Expects an array (or Traversable object) of type/value pairs.
*
* @param array|Traversable $headers
* @throws Exception\InvalidArgumentException
* @return Headers
*/
public function addHeaders($headers)
{
if (! is_array($headers) && ! $headers instanceof Traversable) {
throw new Exception\InvalidArgumentException(sprintf(
'Expected array or Traversable; received "%s"',
is_object($headers) ? $headers::class : gettype($headers)
));
}
foreach ($headers as $name => $value) {
if (is_int($name)) {
if (is_string($value)) {
$this->addHeaderLine($value);
} elseif (is_array($value) && count($value) == 1) {
$this->addHeaderLine(key($value), current($value));
} elseif (is_array($value) && count($value) == 2) {
$this->addHeaderLine($value[0], $value[1]);
} elseif ($value instanceof Header\HeaderInterface) {
$this->addHeader($value);
}
} elseif (is_string($name)) {
$this->addHeaderLine($name, $value);
}
}
return $this;
}
/**
* Add a raw header line, either in name => value, or as a single string 'name: value'
*
* This method allows for lazy-loading in that the parsing and instantiation of HeaderInterface object
* will be delayed until they are retrieved by either get() or current()
*
* @throws Exception\InvalidArgumentException
* @param string $headerFieldNameOrLine
* @param string $fieldValue optional
* @return Headers
*/
public function addHeaderLine($headerFieldNameOrLine, $fieldValue = null)
{
if (! is_string($headerFieldNameOrLine)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects its first argument to be a string; received "%s"',
__METHOD__,
is_object($headerFieldNameOrLine)
? $headerFieldNameOrLine::class
: gettype($headerFieldNameOrLine)
));
}
if ($fieldValue === null) {
$headers = $this->loadHeader($headerFieldNameOrLine);
$headers = is_array($headers) ? $headers : [$headers];
foreach ($headers as $header) {
$this->addHeader($header);
}
} elseif (is_array($fieldValue)) {
foreach ($fieldValue as $i) {
$this->addHeader(Header\GenericMultiHeader::fromString($headerFieldNameOrLine . ':' . $i));
}
} else {
$this->addHeader(GenericHeader::fromString($headerFieldNameOrLine . ':' . $fieldValue));
}
return $this;
}
/**
* Add a Header\Interface to this container, for raw values see {@link addHeaderLine()} and {@link addHeaders()}
*
* @return Headers
*/
public function addHeader(HeaderInterface $header)
{
$key = $this->normalizeFieldName($header->getFieldName());
$this->headersKeys[] = $key;
$this->headers[] = $header;
if ($this->getEncoding() !== 'ASCII') {
$header->setEncoding($this->getEncoding());
}
return $this;
}
/**
* Remove a Header from the container
*
* @param string|HeaderInterface $instanceOrFieldName field name or specific header instance to remove
* @return bool
*/
public function removeHeader($instanceOrFieldName)
{
if (! $instanceOrFieldName instanceof Header\HeaderInterface && ! is_string($instanceOrFieldName)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s requires a string or %s instance; received %s',
__METHOD__,
HeaderInterface::class,
is_object($instanceOrFieldName) ? $instanceOrFieldName::class : gettype($instanceOrFieldName)
));
}
if ($instanceOrFieldName instanceof Header\HeaderInterface) {
$indexes = array_keys($this->headers, $instanceOrFieldName, true);
}
if (is_string($instanceOrFieldName)) {
$key = $this->normalizeFieldName($instanceOrFieldName);
$indexes = array_keys($this->headersKeys, $key, true);
}
if (! empty($indexes)) {
foreach ($indexes as $index) {
unset($this->headersKeys[$index]);
unset($this->headers[$index]);
}
return true;
}
return false;
}
/**
* Clear all headers
*
* Removes all headers from queue
*
* @return Headers
*/
public function clearHeaders()
{
$this->headers = $this->headersKeys = [];
return $this;
}
/**
* Get all headers of a certain name/type
*
* @param string $name
* @return false|ArrayIterator|HeaderInterface Returns false if there is no headers with $name in this
* contain, an ArrayIterator if the header is a MultipleHeadersInterface instance and finally returns
* HeaderInterface for the rest of cases.
*/
public function get($name)
{
$key = $this->normalizeFieldName($name);
$results = [];
foreach (array_keys($this->headersKeys, $key, true) as $index) {
if ($this->headers[$index] instanceof Header\GenericHeader) {
$results[] = $this->lazyLoadHeader($index);
} else {
$results[] = $this->headers[$index];
}
}
switch (count($results)) {
case 0:
return false;
case 1:
if ($results[0] instanceof Header\MultipleHeadersInterface) {
return new ArrayIterator($results);
}
return $results[0];
default:
return new ArrayIterator($results);
}
}
/**
* Test for existence of a type of header
*
* @param string $name
* @return bool
*/
public function has($name)
{
$name = $this->normalizeFieldName($name);
return in_array($name, $this->headersKeys, true);
}
/**
* Advance the pointer for this object as an iterator
*/
#[ReturnTypeWillChange]
public function next()
{
next($this->headers);
}
/**
* Return the current key for this object as an iterator
*
* @return mixed
*/
#[ReturnTypeWillChange]
public function key()
{
return key($this->headers);
}
/**
* Is this iterator still valid?
*
* @return bool
*/
#[ReturnTypeWillChange]
public function valid()
{
return current($this->headers) !== false;
}
/**
* Reset the internal pointer for this object as an iterator
*/
#[ReturnTypeWillChange]
public function rewind()
{
reset($this->headers);
}
/**
* Return the current value for this iterator, lazy loading it if need be
*
* @return HeaderInterface
*/
#[ReturnTypeWillChange]
public function current()
{
$current = current($this->headers);
if ($current instanceof Header\GenericHeader) {
$current = $this->lazyLoadHeader(key($this->headers));
}
return $current;
}
/**
* Return the number of headers in this contain, if all headers have not been parsed, actual count could
* increase if MultipleHeader objects exist in the Request/Response. If you need an exact count, iterate
*
* @return int count of currently known headers
*/
#[ReturnTypeWillChange]
public function count()
{
return count($this->headers);
}
/**
* Render all headers at once
*
* This method handles the normal iteration of headers; it is up to the
* concrete classes to prepend with the appropriate status/request line.
*
* @return string
*/
public function toString()
{
$headers = '';
foreach ($this as $header) {
if ($str = $header->toString()) {
$headers .= $str . self::EOL;
}
}
return $headers;
}
/**
* Return the headers container as an array
*
* @param bool $format Return the values in Mime::Encoded or in Raw format
* @return array<string, list<string>|string>
* @todo determine how to produce single line headers, if they are supported
*/
public function toArray($format = HeaderInterface::FORMAT_RAW)
{
$headers = [];
foreach ($this->headers as $header) {
if ($header instanceof Header\MultipleHeadersInterface) {
$name = $header->getFieldName();
if (! isset($headers[$name])) {
$headers[$name] = [];
}
$headers[$name][] = $header->getFieldValue($format);
} else {
$headers[$header->getFieldName()] = $header->getFieldValue($format);
}
}
return $headers;
}
/**
* By calling this, it will force parsing and loading of all headers, after this count() will be accurate
*
* @return bool
*/
public function forceLoading()
{
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedForeach
foreach ($this as $item) {
// $item should now be loaded
}
return true;
}
/**
* Create Header object from header line
*
* @param string $headerLine
* @return HeaderInterface|HeaderInterface[]
*/
public function loadHeader($headerLine)
{
[$name] = GenericHeader::splitHeaderLine($headerLine);
$class = $this->resolveHeaderClass($name);
assert(null !== $class);
return $class::fromString($headerLine);
}
/**
* @param array-key $index
* @return mixed
*/
protected function lazyLoadHeader($index)
{
$current = $this->headers[$index];
$key = $this->headersKeys[$index];
$class = $this->resolveHeaderClass($key);
assert(null !== $class);
$encoding = $current->getEncoding();
$headers = $class::fromString($current->toString());
if (is_array($headers)) {
$current = array_shift($headers);
assert($current instanceof HeaderInterface);
$current->setEncoding($encoding);
$this->headers[$index] = $current;
foreach ($headers as $header) {
assert($header instanceof HeaderInterface);
$header->setEncoding($encoding);
$this->headersKeys[] = $key;
$this->headers[] = $header;
}
return $current;
}
$current = $headers;
$current->setEncoding($encoding);
$this->headers[$index] = $current;
return $current;
}
/**
* Normalize a field name
*
* @param string $fieldName
* @return string
*/
protected function normalizeFieldName($fieldName)
{
return str_replace(['-', '_', ' ', '.'], '', strtolower($fieldName));
}
/**
* @param string $key
* @return null|class-string<HeaderInterface>
*/
private function resolveHeaderClass($key): ?string
{
if ($this->pluginClassLoader) {
return $this->pluginClassLoader->load($key) ?: GenericHeader::class;
}
return $this->getHeaderLocator()->get($key, GenericHeader::class);
}
}

View File

@@ -1,581 +0,0 @@
<?php
namespace Laminas\Mail;
use ArrayIterator;
use Laminas\Mail\Header\Bcc;
use Laminas\Mail\Header\Cc;
use Laminas\Mail\Header\ContentType;
use Laminas\Mail\Header\From;
use Laminas\Mail\Header\MimeVersion;
use Laminas\Mail\Header\ReplyTo;
use Laminas\Mail\Header\Sender;
use Laminas\Mail\Header\To;
use Laminas\Mime;
use Traversable;
use function array_shift;
use function count;
use function date;
use function gettype;
use function is_array;
use function is_object;
use function is_string;
use function method_exists;
use function sprintf;
class Message
{
/**
* Content of the message
*
* @var string|object|Mime\Message
*/
protected $body;
/** @var Headers */
protected $headers;
/**
* Message encoding
*
* Used to determine whether or not to encode headers; defaults to ASCII.
*
* @var string
*/
protected $encoding = 'ASCII';
/**
* Is the message valid?
*
* If we don't any From addresses, we're invalid, according to RFC2822.
*
* @return bool
*/
public function isValid()
{
$from = $this->getFrom();
if (! $from instanceof AddressList) {
return false;
}
return (bool) count($from);
}
/**
* Set the message encoding
*
* @param string $encoding
* @return Message
*/
public function setEncoding($encoding)
{
$this->encoding = $encoding;
$this->getHeaders()->setEncoding($encoding);
return $this;
}
/**
* Get the message encoding
*
* @return string
*/
public function getEncoding()
{
return $this->encoding;
}
/**
* Compose headers
*
* @return Message
*/
public function setHeaders(Headers $headers)
{
$this->headers = $headers;
$headers->setEncoding($this->getEncoding());
return $this;
}
/**
* Access headers collection
*
* Lazy-loads if not already attached.
*
* @return Headers
*/
public function getHeaders()
{
if (null === $this->headers) {
$this->setHeaders(new Headers());
$date = Header\Date::fromString('Date: ' . date('r'));
$this->headers->addHeader($date);
}
return $this->headers;
}
/**
* Set (overwrite) From addresses
*
* @param string|Address\AddressInterface|array|AddressList|Traversable $emailOrAddressList
* @param string|null $name
* @return Message
*/
public function setFrom($emailOrAddressList, $name = null)
{
$this->clearHeaderByName('from');
return $this->addFrom($emailOrAddressList, $name);
}
/**
* Add a "From" address
*
* @param string|Address|array|AddressList|Traversable $emailOrAddressOrList
* @param string|null $name
* @return Message
*/
public function addFrom($emailOrAddressOrList, $name = null)
{
$addressList = $this->getFrom();
$this->updateAddressList($addressList, $emailOrAddressOrList, $name, __METHOD__);
return $this;
}
/**
* Retrieve list of From senders
*
* @return AddressList
*/
public function getFrom()
{
return $this->getAddressListFromHeader('from', From::class);
}
/**
* Overwrite the address list in the To recipients
*
* @param string|Address\AddressInterface|array|AddressList|Traversable $emailOrAddressList
* @param null|string $name
* @return Message
*/
public function setTo($emailOrAddressList, $name = null)
{
$this->clearHeaderByName('to');
return $this->addTo($emailOrAddressList, $name);
}
/**
* Add one or more addresses to the To recipients
*
* Appends to the list.
*
* @param string|Address\AddressInterface|array|AddressList|Traversable $emailOrAddressOrList
* @param null|string $name
* @return Message
*/
public function addTo($emailOrAddressOrList, $name = null)
{
$addressList = $this->getTo();
$this->updateAddressList($addressList, $emailOrAddressOrList, $name, __METHOD__);
return $this;
}
/**
* Access the address list of the To header
*
* @return AddressList
*/
public function getTo()
{
return $this->getAddressListFromHeader('to', To::class);
}
/**
* Set (overwrite) CC addresses
*
* @param string|Address\AddressInterface|array|AddressList|Traversable $emailOrAddressList
* @param string|null $name
* @return Message
*/
public function setCc($emailOrAddressList, $name = null)
{
$this->clearHeaderByName('cc');
return $this->addCc($emailOrAddressList, $name);
}
/**
* Add a "Cc" address
*
* @param string|Address|array|AddressList|Traversable $emailOrAddressOrList
* @param string|null $name
* @return Message
*/
public function addCc($emailOrAddressOrList, $name = null)
{
$addressList = $this->getCc();
$this->updateAddressList($addressList, $emailOrAddressOrList, $name, __METHOD__);
return $this;
}
/**
* Retrieve list of CC recipients
*
* @return AddressList
*/
public function getCc()
{
return $this->getAddressListFromHeader('cc', Cc::class);
}
/**
* Set (overwrite) BCC addresses
*
* @param string|Address\AddressInterface|array|AddressList|Traversable $emailOrAddressList
* @param string|null $name
* @return Message
*/
public function setBcc($emailOrAddressList, $name = null)
{
$this->clearHeaderByName('bcc');
return $this->addBcc($emailOrAddressList, $name);
}
/**
* Add a "Bcc" address
*
* @param string|Address|array|AddressList|Traversable $emailOrAddressOrList
* @param string|null $name
* @return Message
*/
public function addBcc($emailOrAddressOrList, $name = null)
{
$addressList = $this->getBcc();
$this->updateAddressList($addressList, $emailOrAddressOrList, $name, __METHOD__);
return $this;
}
/**
* Retrieve list of BCC recipients
*
* @return AddressList
*/
public function getBcc()
{
return $this->getAddressListFromHeader('bcc', Bcc::class);
}
/**
* Overwrite the address list in the Reply-To recipients
*
* @param string|Address\AddressInterface|array|AddressList|Traversable $emailOrAddressList
* @param null|string $name
* @return Message
*/
public function setReplyTo($emailOrAddressList, $name = null)
{
$this->clearHeaderByName('reply-to');
return $this->addReplyTo($emailOrAddressList, $name);
}
/**
* Add one or more addresses to the Reply-To recipients
*
* Appends to the list.
*
* @param string|Address\AddressInterface|array|AddressList|Traversable $emailOrAddressOrList
* @param null|string $name
* @return Message
*/
public function addReplyTo($emailOrAddressOrList, $name = null)
{
$addressList = $this->getReplyTo();
$this->updateAddressList($addressList, $emailOrAddressOrList, $name, __METHOD__);
return $this;
}
/**
* Access the address list of the Reply-To header
*
* @return AddressList
*/
public function getReplyTo()
{
return $this->getAddressListFromHeader('reply-to', ReplyTo::class);
}
/**
* setSender
*
* @return Message
*/
public function setSender(mixed $emailOrAddress, mixed $name = null)
{
/** @var Sender $header */
$header = $this->getHeaderByName('sender', Sender::class);
$header->setAddress($emailOrAddress, $name);
return $this;
}
/**
* Retrieve the sender address, if any
*
* @return null|Address\AddressInterface
*/
public function getSender()
{
$headers = $this->getHeaders();
if (! $headers->has('sender')) {
return null;
}
/** @var Sender $header */
$header = $this->getHeaderByName('sender', Sender::class);
return $header->getAddress();
}
/**
* Set the message subject header value
*
* @param string $subject
* @return Message
*/
public function setSubject($subject)
{
$headers = $this->getHeaders();
if (! $headers->has('subject')) {
$header = new Header\Subject();
$headers->addHeader($header);
} else {
$header = $headers->get('subject');
}
$header->setSubject($subject);
$header->setEncoding($this->getEncoding());
return $this;
}
/**
* Get the message subject header value
*
* @return null|string
*/
public function getSubject()
{
$headers = $this->getHeaders();
if (! $headers->has('subject')) {
return;
}
$header = $headers->get('subject');
return $header->getFieldValue();
}
/**
* Set the message body
*
* @param null|string|\Laminas\Mime\Message|object $body
* @throws Exception\InvalidArgumentException
* @return Message
*/
public function setBody($body)
{
if (! is_string($body) && $body !== null) {
if (! is_object($body)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a string or object argument; received "%s"',
__METHOD__,
gettype($body)
));
}
if (! $body instanceof Mime\Message) {
if (! method_exists($body, '__toString')) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects object arguments of type %s or implementing __toString();'
. ' object of type "%s" received',
__METHOD__,
Mime\Message::class,
$body::class
));
}
}
}
$this->body = $body;
if (! $this->body instanceof Mime\Message) {
return $this;
}
// Get headers, and set Mime-Version header
$headers = $this->getHeaders();
$this->getHeaderByName('mime-version', MimeVersion::class);
// Multipart content headers
if ($this->body->isMultiPart()) {
$mime = $this->body->getMime();
/** @var ContentType $header */
$header = $this->getHeaderByName('content-type', ContentType::class);
$header->setType('multipart/mixed');
$header->addParameter('boundary', $mime->boundary());
return $this;
}
// MIME single part headers
$parts = $this->body->getParts();
if (! empty($parts)) {
$part = array_shift($parts);
$headers->addHeaders($part->getHeadersArray("\r\n"));
}
return $this;
}
/**
* Return the currently set message body
*
* @return object|string|Mime\Message
*/
public function getBody()
{
return $this->body;
}
/**
* Get the string-serialized message body text
*
* @return string
*/
public function getBodyText()
{
if ($this->body instanceof Mime\Message) {
return $this->body->generateMessage(Headers::EOL);
}
return (string) $this->body;
}
/**
* Retrieve a header by name
*
* If not found, instantiates one based on $headerClass.
*
* @param string $headerName
* @param string $headerClass
* @return Header\HeaderInterface|ArrayIterator header instance or collection of headers
*/
protected function getHeaderByName($headerName, $headerClass)
{
$headers = $this->getHeaders();
if ($headers->has($headerName)) {
$header = $headers->get($headerName);
} else {
$header = new $headerClass();
$headers->addHeader($header);
}
return $header;
}
/**
* Clear a header by name
*
* @param string $headerName
*/
protected function clearHeaderByName($headerName)
{
$this->getHeaders()->removeHeader($headerName);
}
/**
* Retrieve the AddressList from a named header
*
* Used with To, From, Cc, Bcc, and ReplyTo headers. If the header does not
* exist, instantiates it.
*
* @param string $headerName
* @param string $headerClass
* @throws Exception\DomainException
* @return AddressList
*/
protected function getAddressListFromHeader($headerName, $headerClass)
{
$header = $this->getHeaderByName($headerName, $headerClass);
if (! $header instanceof Header\AbstractAddressList) {
throw new Exception\DomainException(sprintf(
'Cannot grab address list from header of type "%s"; not an AbstractAddressList implementation',
$header::class
));
}
return $header->getAddressList();
}
/**
* Update an address list
*
* Proxied to this from addFrom, addTo, addCc, addBcc, and addReplyTo.
*
* @param string|Address\AddressInterface|array|AddressList|Traversable $emailOrAddressOrList
* @param null|string $name
* @param string $callingMethod
* @throws Exception\InvalidArgumentException
*/
protected function updateAddressList(AddressList $addressList, $emailOrAddressOrList, $name, $callingMethod)
{
if ($emailOrAddressOrList instanceof Traversable) {
foreach ($emailOrAddressOrList as $address) {
$addressList->add($address);
}
return;
}
if (is_array($emailOrAddressOrList)) {
$addressList->addMany($emailOrAddressOrList);
return;
}
if (! is_string($emailOrAddressOrList) && ! $emailOrAddressOrList instanceof Address\AddressInterface) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a string, AddressInterface, array, AddressList, or Traversable as its first argument;'
. ' received "%s"',
$callingMethod,
is_object($emailOrAddressOrList) ? $emailOrAddressOrList::class : gettype($emailOrAddressOrList)
));
}
if (is_string($emailOrAddressOrList) && $name === null) {
$addressList->addFromString($emailOrAddressOrList);
return;
}
$addressList->add($emailOrAddressOrList, $name);
}
/**
* Serialize to string
*
* @return string
*/
public function toString()
{
$headers = $this->getHeaders();
return $headers->toString()
. Headers::EOL
. $this->getBodyText();
}
/**
* Instantiate from raw message string
*
* @todo Restore body to Mime\Message
* @param string $rawMessage
* @return Message
*/
public static function fromString($rawMessage)
{
$message = new static();
/** @var Headers $headers */
$headers = null;
$content = null;
Mime\Decode::splitMessage($rawMessage, $headers, $content, Headers::EOL);
// if ($headers->has('mime-version')) {
// todo - restore body to mime\message
// }
$message->setHeaders($headers);
$message->setBody($content);
return $message;
}
}

View File

@@ -1,67 +0,0 @@
<?php
namespace Laminas\Mail;
use Traversable;
use function gettype;
use function is_array;
use function is_object;
use function method_exists;
use function sprintf;
use function str_replace;
use function strtr;
use function ucwords;
class MessageFactory
{
/**
* @param array|Traversable $options
* @return Message
*/
public static function getInstance($options = [])
{
if (! is_array($options) && ! $options instanceof Traversable) {
throw new Exception\InvalidArgumentException(sprintf(
'"%s" expects an array or Traversable; received "%s"',
__METHOD__,
is_object($options) ? $options::class : gettype($options)
));
}
$message = new Message();
foreach ($options as $key => $value) {
$setter = self::getSetterMethod($key);
if (method_exists($message, $setter)) {
$message->{$setter}($value);
}
}
return $message;
}
/**
* Generate a setter method name based on a provided key.
*
* @param string $key
* @return string
*/
private static function getSetterMethod($key)
{
return 'set'
. str_replace(
' ',
'',
ucwords(
strtr(
$key,
[
'-' => ' ',
'_' => ' ',
]
)
)
);
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace Laminas\Mail;
class Module
{
/**
* Retrieve laminas-mail package configuration for laminas-mvc context.
*
* @return array
*/
public function getConfig()
{
$provider = new ConfigProvider();
return [
'service_manager' => $provider->getDependencyConfig(),
];
}
}

View File

@@ -1,360 +0,0 @@
<?php
namespace Laminas\Mail\Protocol;
use Laminas\Validator;
use Laminas\Validator\ValidatorChain;
use function array_shift;
use function count;
use function fclose;
use function fgets;
use function fwrite;
use function implode;
use function in_array;
use function is_array;
use function is_resource;
use function preg_split;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use function str_starts_with;
use function stream_get_meta_data;
use function stream_set_timeout;
use function stream_socket_client;
use const E_WARNING;
use const PREG_SPLIT_DELIM_CAPTURE;
/**
* Provides low-level methods for concrete adapters to communicate with a
* remote mail server and track requests and responses.
*
* @todo Implement proxy settings
*/
abstract class AbstractProtocol
{
/**
* Mail default EOL string
*/
public const EOL = "\r\n";
/**
* Default timeout in seconds for initiating session
*/
public const TIMEOUT_CONNECTION = 30;
/**
* Maximum of the transaction log
*
* @var int
*/
protected $maximumLog = 64;
/**
* Hostname or IP address of remote server
*
* @var string
*/
protected $host;
/**
* Instance of Laminas\Validator\ValidatorChain to check hostnames
*
* @var ValidatorChain
*/
protected $validHost;
/**
* Socket connection resource
*
* @var null|resource
*/
protected $socket;
/**
* Last request sent to server
*
* @var string
*/
protected $request;
/**
* Array of server responses to last request
*
* @var array
*/
protected $response;
/**
* Log of mail requests and server responses for a session
*/
private array $log = [];
/**
* @param string $host OPTIONAL Hostname of remote connection (default: 127.0.0.1)
* @param int $port OPTIONAL Port number (default: null)
* @throws Exception\RuntimeException
*/
public function __construct($host = '127.0.0.1', protected $port = null)
{
$this->validHost = new Validator\ValidatorChain();
$this->validHost->attach(new Validator\Hostname(Validator\Hostname::ALLOW_ALL));
if (! $this->validHost->isValid($host)) {
throw new Exception\RuntimeException(implode(', ', $this->validHost->getMessages()));
}
$this->host = $host;
}
/**
* Class destructor to cleanup open resources
*/
public function __destruct()
{
$this->_disconnect();
}
/**
* Set the maximum log size
*
* @param int $maximumLog Maximum log size
*/
public function setMaximumLog($maximumLog)
{
$this->maximumLog = (int) $maximumLog;
}
/**
* Get the maximum log size
*
* @return int the maximum log size
*/
public function getMaximumLog()
{
return $this->maximumLog;
}
/**
* Create a connection to the remote host
*
* Concrete adapters for this class will implement their own unique connect
* scripts, using the _connect() method to create the socket resource.
*/
abstract public function connect();
/**
* Retrieve the last client request
*
* @return string
*/
public function getRequest()
{
return $this->request;
}
/**
* Retrieve the last server response
*
* @return array
*/
public function getResponse()
{
return $this->response;
}
/**
* Retrieve the transaction log
*
* @return string
*/
public function getLog()
{
return implode('', $this->log);
}
/**
* Reset the transaction log
*/
public function resetLog()
{
$this->log = [];
}
/**
* Add the transaction log
*
* @param string $value new transaction
*/
// @codingStandardsIgnoreLine PSR2.Methods.MethodDeclaration.Underscore
protected function _addLog($value)
{
if ($this->maximumLog >= 0 && count($this->log) >= $this->maximumLog) {
array_shift($this->log);
}
$this->log[] = $value;
}
/**
* Connect to the server using the supplied transport and target
*
* An example $remote string may be 'tcp://mail.example.com:25' or 'ssh://hostname.com:2222'
*
* @deprecated Since 1.12.0. Implementations should use the ProtocolTrait::setupSocket() method instead.
*
* @todo Remove for 3.0.0.
* @param string $remote Remote
* @throws Exception\RuntimeException
* @return bool
*/
// @codingStandardsIgnoreLine PSR2.Methods.MethodDeclaration.Underscore
protected function _connect($remote)
{
$errorNum = 0;
$errorStr = '';
// open connection
set_error_handler(
static function ($error, $message = '') {
throw new Exception\RuntimeException(sprintf('Could not open socket: %s', $message), $error);
},
E_WARNING
);
$this->socket = stream_socket_client($remote, $errorNum, $errorStr, self::TIMEOUT_CONNECTION);
restore_error_handler();
if ($this->socket === false) {
if ($errorNum == 0) {
$errorStr = 'Could not open socket';
}
throw new Exception\RuntimeException($errorStr);
}
if (($result = stream_set_timeout($this->socket, self::TIMEOUT_CONNECTION)) === false) {
throw new Exception\RuntimeException('Could not set stream timeout');
}
return $result;
}
/**
* Disconnect from remote host and free resource
*/
// @codingStandardsIgnoreLine PSR2.Methods.MethodDeclaration.Underscore
protected function _disconnect()
{
if (is_resource($this->socket)) {
fclose($this->socket);
}
}
/**
* Send the given request followed by a LINEEND to the server.
*
* @param string $request
* @throws Exception\RuntimeException
* @return int|bool Number of bytes written to remote host
*/
// @codingStandardsIgnoreLine PSR2.Methods.MethodDeclaration.Underscore
protected function _send($request)
{
if (! is_resource($this->socket)) {
throw new Exception\RuntimeException('No connection has been established to ' . $this->host);
}
$this->request = $request;
$result = fwrite($this->socket, $request . self::EOL);
// Save request to internal log
$this->_addLog($request . self::EOL);
if ($result === false) {
throw new Exception\RuntimeException('Could not send request to ' . $this->host);
}
return $result;
}
/**
* Get a line from the stream.
*
* @param int $timeout Per-request timeout value if applicable
* @throws Exception\RuntimeException
* @return string
*/
// @codingStandardsIgnoreLine PSR2.Methods.MethodDeclaration.Underscore
protected function _receive($timeout = null)
{
if (! is_resource($this->socket)) {
throw new Exception\RuntimeException('No connection has been established to ' . $this->host);
}
// Adapters may wish to supply per-commend timeouts according to appropriate RFC
if ($timeout !== null) {
stream_set_timeout($this->socket, $timeout);
}
// Retrieve response
$response = fgets($this->socket, 1024);
// Save request to internal log
$this->_addLog($response);
// Check meta data to ensure connection is still valid
$info = stream_get_meta_data($this->socket);
if ($info['timed_out']) {
throw new Exception\RuntimeException($this->host . ' has timed out');
}
if ($response === false) {
throw new Exception\RuntimeException('Could not read from ' . $this->host);
}
return $response;
}
/**
* Parse server response for successful codes
*
* Read the response from the stream and check for expected return code.
* Throws a Laminas\Mail\Protocol\Exception\ExceptionInterface if an unexpected code is returned.
*
* @param string|array $code One or more codes that indicate a successful response
* @param int $timeout Per-request timeout value if applicable
* @throws Exception\RuntimeException
* @return string Last line of response string
*/
// @codingStandardsIgnoreLine PSR2.Methods.MethodDeclaration.Underscore
protected function _expect($code, $timeout = null)
{
$this->response = [];
$errMsg = '';
if (! is_array($code)) {
$code = [$code];
}
do {
$this->response[] = $result = $this->_receive($timeout);
[$cmd, $more, $msg] = preg_split('/([\s-]+)/', $result, 2, PREG_SPLIT_DELIM_CAPTURE);
if ($errMsg !== '') {
$errMsg .= ' ' . $msg;
} elseif ($cmd === null || ! in_array($cmd, $code)) {
$errMsg = $msg;
}
// The '-' message prefix indicates an information string instead of a response string.
} while (str_starts_with($more, '-'));
if ($errMsg !== '') {
throw new Exception\RuntimeException($errMsg, (int) $cmd);
}
return $msg;
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Mail\Protocol\Exception;
use Laminas\Mail\Exception\ExceptionInterface as MailException;
interface ExceptionInterface extends MailException
{
}

View File

@@ -1,12 +0,0 @@
<?php
namespace Laminas\Mail\Protocol\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -1,12 +0,0 @@
<?php
namespace Laminas\Mail\Protocol\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}

View File

@@ -1,856 +0,0 @@
<?php
namespace Laminas\Mail\Protocol;
use Laminas\Mail\Protocol\Exception\ExceptionInterface;
use function array_merge;
use function array_pop;
use function array_push;
use function array_search;
use function array_shift;
use function count;
use function current;
use function explode;
use function fclose;
use function fgets;
use function func_get_args;
use function func_num_args;
use function fwrite;
use function implode;
use function is_array;
use function is_numeric;
use function key;
use function next;
use function preg_match;
use function rtrim;
use function str_contains;
use function str_replace;
use function str_starts_with;
use function stream_socket_enable_crypto;
use function strlen;
use function strpos;
use function strtolower;
use function substr;
use function trim;
use const INF;
class Imap
{
use ProtocolTrait;
/**
* Default timeout in seconds for initiating session
*/
public const TIMEOUT_CONNECTION = 30;
/** @var null|resource */
protected $socket;
/**
* counter for request tag
*
* @var int
*/
protected $tagCount = 0;
/**
* Public constructor
*
* @param string $host hostname or IP address of IMAP server, if given connect() is called
* @param int|null $port port of IMAP server, null for default (143 or 993 for ssl)
* @param string|bool $ssl use ssl? 'SSL', 'TLS' or false
* @param bool $novalidatecert set to true to skip SSL certificate validation
* @throws ExceptionInterface
*/
public function __construct($host = '', $port = null, $ssl = false, $novalidatecert = false)
{
$this->setNoValidateCert($novalidatecert);
if ($host) {
$this->connect($host, $port, $ssl);
}
}
/**
* Public destructor
*/
public function __destruct()
{
$this->logout();
}
/**
* Open connection to IMAP server
*
* @param string $host hostname or IP address of IMAP server
* @param int|null $port of IMAP server, default is 143 (993 for ssl)
* @param string|bool $ssl use 'SSL', 'TLS' or false
* @throws Exception\RuntimeException
* @return void
*/
public function connect($host, $port = null, $ssl = false)
{
$transport = 'tcp';
$isTls = false;
if ($ssl) {
$ssl = strtolower($ssl);
}
switch ($ssl) {
case 'ssl':
$transport = 'ssl';
if (! $port) {
$port = 993;
}
break;
case 'tls':
$isTls = true;
// break intentionally omitted
default:
if (! $port) {
$port = 143;
}
}
$this->socket = $this->setupSocket($transport, $host, $port, self::TIMEOUT_CONNECTION);
if (! $this->assumedNextLine('* OK')) {
throw new Exception\RuntimeException('host doesn\'t allow connection');
}
if ($isTls) {
$result = $this->requestAndResponse('STARTTLS');
$result = $result && stream_socket_enable_crypto($this->socket, true, $this->getCryptoMethod());
if (! $result) {
throw new Exception\RuntimeException('cannot enable TLS');
}
}
}
/**
* get the next line from socket with error checking, but nothing else
*
* @throws Exception\RuntimeException
* @return string next line
*/
protected function nextLine()
{
$line = fgets($this->socket);
if ($line === false) {
throw new Exception\RuntimeException('cannot read - connection closed?');
}
return $line;
}
/**
* get next line and assume it starts with $start. some requests give a simple
* feedback so we can quickly check if we can go on.
*
* @param string $start the first bytes we assume to be in the next line
* @return bool line starts with $start
*/
protected function assumedNextLine($start)
{
$line = $this->nextLine();
return str_starts_with($line, $start);
}
/**
* get next line and split the tag. that's the normal case for a response line
*
* @param string $tag tag of line is returned by reference
* @return string next line
*/
protected function nextTaggedLine(&$tag)
{
$line = $this->nextLine();
// separate tag from line
[$tag, $line] = explode(' ', $line, 2);
return $line;
}
/**
* split a given line in tokens. a token is literal of any form or a list
*
* @param string $line line to decode
* @return array tokens, literals are returned as string, lists as array
*/
protected function decodeLine($line)
{
$tokens = [];
$stack = [];
/*
We start to decode the response here. The understood tokens are:
literal
"literal" or also "lit\\er\"al"
{bytes}<NL>literal
(literals*)
All tokens are returned in an array. Literals in braces (the last understood
token in the list) are returned as an array of tokens. I.e. the following response:
"foo" baz {3}<NL>bar ("f\\\"oo" bar)
would be returned as:
array('foo', 'baz', 'bar', array('f\\\"oo', 'bar'));
// TODO: add handling of '[' and ']' to parser for easier handling of response text
*/
// replace any trailing <NL> including spaces with a single space
$line = rtrim($line) . ' ';
while (($pos = strpos($line, ' ')) !== false) {
$token = substr($line, 0, $pos);
if (! strlen($token)) {
continue;
}
while ($token[0] == '(') {
array_push($stack, $tokens);
$tokens = [];
$token = substr($token, 1);
}
if ($token[0] == '"') {
if (preg_match('%^\(*"((.|\\\\|\\")*?)" *%', $line, $matches)) {
$tokens[] = $matches[1];
$line = substr($line, strlen($matches[0]));
continue;
}
}
if ($token[0] == '{') {
$endPos = strpos($token, '}');
$chars = substr($token, 1, $endPos - 1);
if (is_numeric($chars)) {
$token = '';
while (strlen($token) < $chars) {
$token .= $this->nextLine();
}
$line = '';
if (strlen($token) > $chars) {
$line = substr($token, $chars);
$token = substr($token, 0, $chars);
} else {
$line .= $this->nextLine();
}
$tokens[] = $token;
$line = trim($line) . ' ';
continue;
}
}
if ($stack && $token[strlen($token) - 1] == ')') {
// closing braces are not separated by spaces, so we need to count them
$braces = strlen($token);
$token = rtrim($token, ')');
// only count braces if more than one
$braces -= strlen($token) + 1;
// only add if token had more than just closing braces
if (rtrim($token) != '') {
$tokens[] = rtrim($token);
}
$token = $tokens;
$tokens = array_pop($stack);
// special handline if more than one closing brace
while ($braces-- > 0) {
$tokens[] = $token;
$token = $tokens;
$tokens = array_pop($stack);
}
}
$tokens[] = $token;
$line = substr($line, $pos + 1);
}
// maybe the server forgot to send some closing braces
while ($stack) {
$child = $tokens;
$tokens = array_pop($stack);
$tokens[] = $child;
}
return $tokens;
}
/**
* read a response "line" (could also be more than one real line if response has {..}<NL>)
* and do a simple decode
*
* @param array|string $tokens decoded tokens are returned by reference, if $dontParse
* is true the unparsed line is returned here
* @param string $wantedTag check for this tag for response code. Default '*' is
* continuation tag.
* @param bool $dontParse if true only the unparsed line is returned $tokens
* @return bool if returned tag matches wanted tag
*/
public function readLine(&$tokens = [], $wantedTag = '*', $dontParse = false)
{
$tag = null; // define $tag variable before first use
$line = $this->nextTaggedLine($tag); // get next tag
if (! $dontParse) {
$tokens = $this->decodeLine($line);
} else {
$tokens = $line;
}
// if tag is wanted tag we might be at the end of a multiline response
return $tag == $wantedTag;
}
/**
* read all lines of response until given tag is found (last line of response)
*
* @param string $tag the tag of your request
* @param bool $dontParse if true every line is returned unparsed instead of
* the decoded tokens
* @return null|bool|array tokens if success, false if error, null if bad request
*/
public function readResponse($tag, $dontParse = false)
{
$lines = [];
$tokens = null; // define $tokens variable before first use
while (! $this->readLine($tokens, $tag, $dontParse)) {
$lines[] = $tokens;
}
if ($dontParse) {
// last to chars are still needed for response code
$tokens = [substr($tokens, 0, 2)];
}
// last line has response code
if ($tokens[0] == 'OK') {
return $lines ?: true;
} elseif ($tokens[0] == 'NO') {
return false;
}
}
/**
* send a request
*
* @param string $command your request command
* @param array $tokens additional parameters to command, use escapeString() to prepare
* @param string $tag provide a tag otherwise an autogenerated is returned
* @throws Exception\RuntimeException
*/
public function sendRequest($command, $tokens = [], &$tag = null)
{
if (! $tag) {
++$this->tagCount;
$tag = 'TAG' . $this->tagCount;
}
$line = $tag . ' ' . $command;
foreach ($tokens as $token) {
if (is_array($token)) {
if (fwrite($this->socket, $line . ' ' . $token[0] . "\r\n") === false) {
throw new Exception\RuntimeException('cannot write - connection closed?');
}
if (! $this->assumedNextLine('+ ')) {
throw new Exception\RuntimeException('cannot send literal string');
}
$line = $token[1];
} else {
$line .= ' ' . $token;
}
}
if (fwrite($this->socket, $line . "\r\n") === false) {
throw new Exception\RuntimeException('cannot write - connection closed?');
}
}
/**
* send a request and get response at once
*
* @param string $command command as in sendRequest()
* @param array $tokens parameters as in sendRequest()
* @param bool $dontParse if true unparsed lines are returned instead of tokens
* @return mixed response as in readResponse()
*/
public function requestAndResponse($command, $tokens = [], $dontParse = false)
{
$tag = null; // define $tag variable before first use
$this->sendRequest($command, $tokens, $tag);
return $this->readResponse($tag, $dontParse);
}
/**
* escape one or more literals i.e. for sendRequest
*
* @param string $string the literal/-s
* @return string|array escape literals, literals with newline ar returned
* as array('{size}', 'string');
*/
public function escapeString($string)
{
if (func_num_args() < 2) {
if (str_contains($string, "\n")) {
return ['{' . strlen($string) . '}', $string];
}
return '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $string) . '"';
}
$result = [];
foreach (func_get_args() as $string) {
$result[] = $this->escapeString($string);
}
return $result;
}
/**
* escape a list with literals or lists
*
* @param array $list list with literals or lists as PHP array
* @return string escaped list for imap
*/
public function escapeList($list)
{
$result = [];
foreach ($list as $v) {
if (! is_array($v)) {
$result[] = $v;
continue;
}
$result[] = $this->escapeList($v);
}
return '(' . implode(' ', $result) . ')';
}
/**
* Login to IMAP server.
*
* @param string $user username
* @param string $password password
* @return bool success
*/
public function login($user, $password)
{
return $this->requestAndResponse('LOGIN', $this->escapeString($user, $password), true);
}
/**
* logout of imap server
*
* @return bool success
*/
public function logout()
{
$result = false;
if ($this->socket) {
try {
$result = $this->requestAndResponse('LOGOUT', [], true);
} catch (Exception\ExceptionInterface) {
// ignoring exception
}
fclose($this->socket);
$this->socket = null;
}
return $result;
}
/**
* Get capabilities from IMAP server
*
* @return array list of capabilities
* @throws ExceptionInterface
*/
public function capability()
{
$response = $this->requestAndResponse('CAPABILITY');
if (! $response) {
return [];
}
$capabilities = [];
foreach ($response as $line) {
$capabilities = array_merge($capabilities, $line);
}
return $capabilities;
}
/**
* Examine and select have the same response. The common code for both
* is in this method
*
* @param string $command can be 'EXAMINE' or 'SELECT' and this is used as command
* @param string $box which folder to change to or examine
* @return bool|array false if error, array with returned information
* otherwise (flags, exists, recent, uidvalidity)
* @throws ExceptionInterface
*/
public function examineOrSelect($command = 'EXAMINE', $box = 'INBOX')
{
$tag = null; // define $tag variable before first use
$this->sendRequest($command, [$this->escapeString($box)], $tag);
$result = [];
$tokens = null; // define $tokens variable before first use
while (! $this->readLine($tokens, $tag)) {
if ($tokens[0] == 'FLAGS') {
array_shift($tokens);
$result['flags'] = $tokens;
continue;
}
switch ($tokens[1]) {
case 'EXISTS':
case 'RECENT':
$result[strtolower($tokens[1])] = $tokens[0];
break;
case '[UIDVALIDITY':
$result['uidvalidity'] = (int) $tokens[2];
break;
default:
// ignore
}
}
if ($tokens[0] != 'OK') {
return false;
}
return $result;
}
/**
* change folder
*
* @param string $box change to this folder
* @return bool|array see examineOrselect()
* @throws ExceptionInterface
*/
public function select($box = 'INBOX')
{
return $this->examineOrSelect('SELECT', $box);
}
/**
* examine folder
*
* @param string $box examine this folder
* @return bool|array see examineOrselect()
* @throws ExceptionInterface
*/
public function examine($box = 'INBOX')
{
return $this->examineOrSelect('EXAMINE', $box);
}
/**
* fetch one or more items of one or more messages
*
* @param string|array $items items to fetch from message(s) as string (if only one item)
* or array of strings
* @param int|array $from message for items or start message if $to !== null
* @param int|null $to if null only one message ($from) is fetched, else it's the
* last message, INF means last message available
* @param bool $uid set to true if passing a unique id
* @throws Exception\RuntimeException
* @return string|array if only one item of one message is fetched it's returned as string
* if items of one message are fetched it's returned as (name => value)
* if one items of messages are fetched it's returned as (msgno => value)
* if items of messages are fetched it's returned as (msgno => (name => value))
*/
public function fetch($items, $from, $to = null, $uid = false)
{
if (is_array($from)) {
$set = implode(',', $from);
} elseif ($to === null) {
$set = (int) $from;
} elseif ($to === INF) {
$set = (int) $from . ':*';
} else {
$set = (int) $from . ':' . (int) $to;
}
$items = (array) $items;
$itemList = $this->escapeList($items);
$tag = null; // define $tag variable before first use
$this->sendRequest(($uid ? 'UID ' : '') . 'FETCH', [$set, $itemList], $tag);
$result = [];
$tokens = null; // define $tokens variable before first use
while (! $this->readLine($tokens, $tag)) {
// ignore other responses
if ($tokens[1] != 'FETCH') {
continue;
}
// find array key of UID value; try the last elements, or search for it
if ($uid) {
$count = count($tokens[2]);
if ($tokens[2][$count - 2] == 'UID') {
$uidKey = $count - 1;
} else {
$uidKey = array_search('UID', $tokens[2]) + 1;
}
}
// ignore other messages
if ($to === null && ! is_array($from) && ($uid ? $tokens[2][$uidKey] != $from : $tokens[0] != $from)) {
continue;
}
// if we only want one item we return that one directly
if (count($items) == 1) {
if ($tokens[2][0] == $items[0]) {
$data = $tokens[2][1];
} elseif ($uid && $tokens[2][2] == $items[0]) {
$data = $tokens[2][3];
} else {
// maybe the server send an other field we didn't wanted
$count = count($tokens[2]);
// we start with 2, because 0 was already checked
for ($i = 2; $i < $count; $i += 2) {
if ($tokens[2][$i] != $items[0]) {
continue;
}
$data = $tokens[2][$i + 1];
break;
}
}
} else {
$data = [];
while (key($tokens[2]) !== null) {
$data[current($tokens[2])] = next($tokens[2]);
next($tokens[2]);
}
}
// if we want only one message we can ignore everything else and just return
if ($to === null && ! is_array($from) && ($uid ? $tokens[2][$uidKey] == $from : $tokens[0] == $from)) {
// we still need to read all lines
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedWhile
while (! $this->readLine($tokens, $tag)) {
}
return $data;
}
$result[$tokens[0]] = $data;
}
if ($to === null && ! is_array($from)) {
throw new Exception\RuntimeException('the single id was not found in response');
}
return $result;
}
/**
* get mailbox list
*
* this method can't be named after the IMAP command 'LIST', as list is a reserved keyword
*
* @param string $reference mailbox reference for list
* @param string $mailbox mailbox name match with wildcards
* @return array mailboxes that matched $mailbox as array(globalName => array('delim' => .., 'flags' => ..))
* @throws ExceptionInterface
*/
public function listMailbox($reference = '', $mailbox = '*')
{
$result = [];
$list = $this->requestAndResponse('LIST', $this->escapeString($reference, $mailbox));
if (! $list || $list === true) {
return $result;
}
foreach ($list as $item) {
if (count($item) != 4 || $item[0] != 'LIST') {
continue;
}
$result[$item[3]] = ['delim' => $item[2], 'flags' => $item[1]];
}
return $result;
}
/**
* set flags
*
* @param array $flags flags to set, add or remove - see $mode
* @param int $from message for items or start message if $to !== null
* @param int|null $to if null only one message ($from) is fetched, else it's the
* last message, INF means last message available
* @param string|null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given
* @param bool $silent if false the return values are the new flags for the wanted messages
* @return bool|array new flags if $silent is false, else true or false depending on success
* @throws ExceptionInterface
*/
public function store(array $flags, $from, $to = null, $mode = null, $silent = true)
{
$item = 'FLAGS';
if ($mode == '+' || $mode == '-') {
$item = $mode . $item;
}
if ($silent) {
$item .= '.SILENT';
}
$flags = $this->escapeList($flags);
$set = (int) $from;
if ($to !== null) {
$set .= ':' . ($to == INF ? '*' : (int) $to);
}
$result = $this->requestAndResponse('STORE', [$set, $item, $flags], $silent);
if ($silent) {
return (bool) $result;
}
$tokens = $result;
$result = [];
foreach ($tokens as $token) {
if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') {
continue;
}
$result[$token[0]] = $token[2][1];
}
return $result;
}
/**
* append a new message to given folder
*
* @param string $folder name of target folder
* @param string $message full message content
* @param array $flags flags for new message
* @param string $date date for new message
* @return bool success
* @throws ExceptionInterface
*/
public function append($folder, $message, $flags = null, $date = null)
{
$tokens = [];
$tokens[] = $this->escapeString($folder);
if ($flags !== null) {
$tokens[] = $this->escapeList($flags);
}
if ($date !== null) {
$tokens[] = $this->escapeString($date);
}
$tokens[] = $this->escapeString($message);
return $this->requestAndResponse('APPEND', $tokens, true);
}
/**
* copy message set from current folder to other folder
*
* @param string $folder destination folder
* @param int $from
* @param int|null $to if null only one message ($from) is fetched, else it's the
* last message, INF means last message available
* @return bool success
*/
public function copy($folder, $from, $to = null)
{
$set = (string) $from;
if ($to !== null) {
$set .= ':' . ($to == INF ? '*' : (int) $to);
}
return $this->requestAndResponse('COPY', [$set, $this->escapeString($folder)], true);
}
/**
* create a new folder (and parent folders if needed)
*
* @param string $folder folder name
* @return bool success
*/
public function create($folder)
{
return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true);
}
/**
* rename an existing folder
*
* @param string $old old name
* @param string $new new name
* @return bool success
*/
public function rename($old, $new)
{
return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true);
}
/**
* remove a folder
*
* @param string $folder folder name
* @return bool success
*/
public function delete($folder)
{
return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true);
}
/**
* subscribe to a folder
*
* @param string $folder folder name
* @return bool success
*/
public function subscribe($folder)
{
return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true);
}
/**
* permanently remove messages
*
* @return bool success
*/
public function expunge()
{
// TODO: parse response?
return $this->requestAndResponse('EXPUNGE');
}
/**
* send noop
*
* @return bool success
*/
public function noop()
{
// TODO: parse response
return $this->requestAndResponse('NOOP');
}
/**
* do a search request
*
* This method is currently marked as internal as the API might change and is not
* safe if you don't take precautions.
*
* @param array $params
* @return array message ids
*/
public function search(array $params)
{
$response = $this->requestAndResponse('SEARCH', $params);
if (! $response) {
return $response;
}
// if there are no messages in the folder, $response is equal to true
if ($response === true) {
return [];
}
foreach ($response as $ids) {
if ($ids[0] == 'SEARCH') {
array_shift($ids);
return $ids;
}
}
return [];
}
}

View File

@@ -1,419 +0,0 @@
<?php
namespace Laminas\Mail\Protocol;
use Laminas\Mail\Protocol\Pop3\Response;
use Laminas\Stdlib\ErrorHandler;
use function explode;
use function fclose;
use function fgets;
use function fwrite;
use function is_string;
use function md5;
use function rtrim;
use function stream_socket_enable_crypto;
use function strpos;
use function strtok;
use function strtolower;
use function substr;
use function trim;
class Pop3
{
use ProtocolTrait;
/**
* Default timeout in seconds for initiating session
*/
public const TIMEOUT_CONNECTION = 30;
/**
* saves if server supports top
*
* @var null|bool
*/
public $hasTop;
/** @var null|resource */
protected $socket;
/**
* greeting timestamp for apop
*
* @var null|string
*/
protected $timestamp;
/**
* Public constructor
*
* @param string $host hostname or IP address of POP3 server, if given connect() is called
* @param int|null $port port of POP3 server, null for default (110 or 995 for ssl)
* @param bool|string $ssl use ssl? 'SSL', 'TLS' or false
* @param bool $novalidatecert set to true to skip SSL certificate validation
*/
public function __construct($host = '', $port = null, $ssl = false, $novalidatecert = false)
{
$this->setNoValidateCert($novalidatecert);
if ($host) {
$this->connect($host, $port, $ssl);
}
}
/**
* Public destructor
*/
public function __destruct()
{
$this->logout();
}
/**
* Open connection to POP3 server
*
* @param string $host hostname or IP address of POP3 server
* @param int|null $port of POP3 server, default is 110 (995 for ssl)
* @param string|bool $ssl use 'SSL', 'TLS' or false
* @throws Exception\RuntimeException
* @return string welcome message
*/
public function connect($host, $port = null, $ssl = false)
{
$transport = 'tcp';
$isTls = false;
if ($ssl) {
$ssl = strtolower($ssl);
}
switch ($ssl) {
case 'ssl':
$transport = 'ssl';
if (! $port) {
$port = 995;
}
break;
case 'tls':
$isTls = true;
// break intentionally omitted
default:
if (! $port) {
$port = 110;
}
}
$this->socket = $this->setupSocket($transport, $host, $port, self::TIMEOUT_CONNECTION);
$welcome = $this->readResponse();
strtok($welcome, '<');
$this->timestamp = strtok('>');
if (! strpos($this->timestamp, '@')) {
$this->timestamp = null;
} else {
$this->timestamp = '<' . $this->timestamp . '>';
}
if ($isTls) {
$this->request('STLS');
$result = stream_socket_enable_crypto($this->socket, true, $this->getCryptoMethod());
if (! $result) {
throw new Exception\RuntimeException('cannot enable TLS');
}
}
return $welcome;
}
/**
* Send a request
*
* @param string $request your request without newline
* @throws Exception\RuntimeException
*/
public function sendRequest($request)
{
ErrorHandler::start();
$result = fwrite($this->socket, $request . "\r\n");
$error = ErrorHandler::stop();
if (! $result) {
throw new Exception\RuntimeException('send failed - connection closed?', 0, $error);
}
}
/**
* read a response
*
* @param bool $multiline response has multiple lines and should be read until "<nl>.<nl>"
* @throws Exception\RuntimeException
* @return string response
*/
public function readResponse($multiline = false)
{
$response = $this->readRemoteResponse();
if ($response->status() != '+OK') {
throw new Exception\RuntimeException('last request failed');
}
$message = $response->message();
if ($multiline) {
$message = '';
$line = fgets($this->socket);
while ($line && rtrim($line, "\r\n") != '.') {
if ($line[0] == '.') {
$line = substr($line, 1);
}
$message .= $line;
$line = fgets($this->socket);
}
}
return $message;
}
/**
* read a response
* return extracted status / message from response
* @throws Exception\RuntimeException
*/
protected function readRemoteResponse(): Response
{
ErrorHandler::start();
$result = fgets($this->socket);
$error = ErrorHandler::stop();
if (! is_string($result)) {
throw new Exception\RuntimeException('read failed - connection closed?', 0, $error);
}
$result = trim($result);
if (strpos($result, ' ')) {
[$status, $message] = explode(' ', $result, 2);
} else {
$status = $result;
$message = '';
}
return new Response($status, $message);
}
/**
* Send request and get response
*
* @see sendRequest()
* @see readResponse()
*
* @param string $request request
* @param bool $multiline multiline response?
* @return string result from readResponse()
*/
public function request($request, $multiline = false)
{
$this->sendRequest($request);
return $this->readResponse($multiline);
}
/**
* End communication with POP3 server (also closes socket)
*/
public function logout()
{
if ($this->socket) {
try {
$this->request('QUIT');
} catch (Exception\ExceptionInterface) {
// ignore error - we're closing the socket anyway
}
fclose($this->socket);
$this->socket = null;
}
}
/**
* Get capabilities from POP3 server
*
* @return array list of capabilities
*/
public function capa()
{
$result = $this->request('CAPA', true);
return explode("\n", $result);
}
/**
* Login to POP3 server. Can use APOP
*
* @param string $user username
* @param string $password password
* @param bool $tryApop should APOP be tried?
*/
public function login($user, $password, $tryApop = true)
{
if ($tryApop && $this->timestamp) {
try {
$this->request("APOP $user " . md5($this->timestamp . $password));
return;
} catch (Exception\ExceptionInterface) {
// ignore
}
}
$this->request("USER $user");
$this->request("PASS $password");
}
/**
* Make STAT call for message count and size sum
*
* @param int $messages out parameter with count of messages
* @param int $octets out parameter with size in octets of messages
*/
public function status(&$messages, &$octets)
{
$messages = 0;
$octets = 0;
$result = $this->request('STAT');
[$messages, $octets] = explode(' ', $result);
}
/**
* Make LIST call for size of message(s)
*
* @param int|null $msgno number of message, null for all
* @return int|array size of given message or list with array(num => size)
*/
public function getList($msgno = null)
{
if ($msgno !== null) {
$result = $this->request("LIST $msgno");
[, $result] = explode(' ', $result);
return (int) $result;
}
$result = $this->request('LIST', true);
$messages = [];
$line = strtok($result, "\n");
while ($line) {
[$no, $size] = explode(' ', trim($line));
$messages[(int) $no] = (int) $size;
$line = strtok("\n");
}
return $messages;
}
/**
* Make UIDL call for getting a uniqueid
*
* @param int|null $msgno number of message, null for all
* @return string|array uniqueid of message or list with array(num => uniqueid)
*/
public function uniqueid($msgno = null)
{
if ($msgno !== null) {
$result = $this->request("UIDL $msgno");
[, $result] = explode(' ', $result);
return $result;
}
$result = $this->request('UIDL', true);
$result = explode("\n", $result);
$messages = [];
foreach ($result as $line) {
if (! $line) {
continue;
}
[$no, $id] = explode(' ', trim($line), 2);
$messages[(int) $no] = $id;
}
return $messages;
}
/**
* Make TOP call for getting headers and maybe some body lines
* This method also sets hasTop - before it it's not known if top is supported
*
* The fallback makes normal RETR call, which retrieves the whole message. Additional
* lines are not removed.
*
* @param int $msgno number of message
* @param int $lines number of wanted body lines (empty line is inserted after header lines)
* @param bool $fallback fallback with full retrieve if top is not supported
* @throws Exception\RuntimeException
* @throws Exception\ExceptionInterface
* @return string message headers with wanted body lines
*/
public function top($msgno, $lines = 0, $fallback = false)
{
if ($this->hasTop === false) {
if ($fallback) {
return $this->retrieve($msgno);
}
throw new Exception\RuntimeException('top not supported and no fallback wanted');
}
$this->hasTop = true;
$lines = ! $lines || $lines < 1 ? 0 : (int) $lines;
try {
$result = $this->request("TOP $msgno $lines", true);
} catch (Exception\ExceptionInterface $e) {
$this->hasTop = false;
if ($fallback) {
$result = $this->retrieve($msgno);
} else {
throw $e;
}
}
return $result;
}
/**
* Make a RETR call for retrieving a full message with headers and body
*
* @param int $msgno message number
* @return string message
*/
public function retrieve($msgno)
{
return $this->request("RETR $msgno", true);
}
/**
* Make a NOOP call, maybe needed for keeping the server happy
*/
public function noop()
{
$this->request('NOOP');
}
/**
* Make a DELE count to remove a message
*
* @param int $msgno
*/
public function delete($msgno)
{
$this->request("DELE $msgno");
}
/**
* Make RSET call, which rollbacks delete requests
*/
public function undelete()
{
$this->request('RSET');
}
}

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Mail\Protocol\Pop3;
/**
* POP3 response value object
*
* @internal
*/
final class Response
{
/** @var string $status */
private $status;
/** @var string $message */
private $message;
public function __construct(string $status, string $message)
{
$this->status = $status;
$this->message = $message;
}
public function status(): string
{
return $this->status;
}
public function message(): string
{
return $this->message;
}
}

View File

@@ -1,34 +0,0 @@
<?php
namespace Laminas\Mail\Protocol\Pop3\Xoauth2;
use Laminas\Mail\Protocol\Exception\RuntimeException;
use Laminas\Mail\Protocol\Pop3;
use Laminas\Mail\Protocol\Xoauth2\Xoauth2;
/**
* @final
*/
class Microsoft extends Pop3
{
protected const AUTH_INITIALIZE_REQUEST = 'AUTH XOAUTH2';
protected const AUTH_RESPONSE_INITIALIZED_OK = '+';
/**
* @param string $user the target mailbox to access
* @param string $password OAUTH2 accessToken
* @param bool $tryApop obsolete parameter not used here
*/
public function login($user, $password, $tryApop = true): void
{
$this->sendRequest(self::AUTH_INITIALIZE_REQUEST);
$response = $this->readRemoteResponse();
if ($response->status() != self::AUTH_RESPONSE_INITIALIZED_OK) {
throw new RuntimeException($response->message());
}
$this->request(Xoauth2::encodeXoauth2Sasl($user, $password));
}
}

View File

@@ -1,122 +0,0 @@
<?php
namespace Laminas\Mail\Protocol;
use Laminas\Stdlib\ErrorHandler;
use function defined;
use function sprintf;
use function stream_context_create;
use function stream_set_timeout;
use function stream_socket_client;
use const STREAM_CLIENT_CONNECT;
use const STREAM_CRYPTO_METHOD_TLS_CLIENT;
use const STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
use const STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
/**
* https://bugs.php.net/bug.php?id=69195
*/
trait ProtocolTrait
{
/**
* If set to true, do not validate the SSL certificate
*
* @var null|bool
*/
protected $novalidatecert;
public function getCryptoMethod(): int
{
// Allow the best TLS version(s) we can
$cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT;
// PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
// so add them back in manually if we can
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
$cryptoMethod |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
$cryptoMethod |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
}
return $cryptoMethod;
}
/**
* Do not validate SSL certificate
*
* @todo Update to return self when minimum supported PHP version is 7.4+
* @param bool $novalidatecert Set to true to disable certificate validation
* @return $this
*/
public function setNoValidateCert(bool $novalidatecert)
{
$this->novalidatecert = $novalidatecert;
return $this;
}
/**
* Should we validate SSL certificate?
*/
public function validateCert(): bool
{
return ! $this->novalidatecert;
}
/**
* Prepare socket options
*
* @return array
*/
private function prepareSocketOptions(): array
{
return $this->novalidatecert
? [
'ssl' => [
'verify_peer_name' => false,
'verify_peer' => false,
],
]
: [];
}
/**
* Setup connection socket
*
* @param string $host hostname or IP address of IMAP server
* @param int|null $port of IMAP server, default is 143 (993 for ssl)
* @param int $timeout timeout in seconds for initiating session
* @return resource The socket created.
* @throws Exception\RuntimeException If unable to connect to host.
*/
protected function setupSocket(
string $transport,
string $host,
?int $port,
int $timeout
) {
ErrorHandler::start();
$socket = stream_socket_client(
sprintf('%s://%s:%d', $transport, $host, $port),
$errno,
$errstr,
$timeout,
STREAM_CLIENT_CONNECT,
stream_context_create($this->prepareSocketOptions())
);
$error = ErrorHandler::stop();
if (! $socket) {
throw new Exception\RuntimeException(sprintf(
'cannot connect to host%s',
$error ? sprintf('; error = %s (errno = %d )', $error->getMessage(), $error->getCode()) : ''
), 0, $error);
}
if (false === stream_set_timeout($socket, $timeout)) {
throw new Exception\RuntimeException('Could not set stream timeout');
}
return $socket;
}
}

View File

@@ -1,521 +0,0 @@
<?php
namespace Laminas\Mail\Protocol;
use Generator;
use Laminas\Mail\Headers;
use function array_key_exists;
use function array_replace_recursive;
use function chunk_split;
use function fclose;
use function fgets;
use function fopen;
use function fwrite;
use function implode;
use function ini_get;
use function is_array;
use function rewind;
use function rtrim;
use function stream_socket_enable_crypto;
use function strlen;
use function strtolower;
use function substr;
/**
* SMTP implementation of Laminas\Mail\Protocol\AbstractProtocol
*
* Minimum implementation according to RFC2821: EHLO, MAIL FROM, RCPT TO, DATA,
* RSET, NOOP, QUIT
*/
class Smtp extends AbstractProtocol
{
use ProtocolTrait;
/**
* RFC 5322 section-2.2.3 specifies maximum of 998 bytes per line.
* This may not be exceeded.
*
* @see https://tools.ietf.org/html/rfc5322#section-2.2.3
*/
public const SMTP_LINE_LIMIT = 998;
/**
* The transport method for the socket
*
* @var string
*/
protected $transport = 'tcp';
/**
* Indicates that a session is requested to be secure
*
* @var string
*/
protected $secure;
/**
* Indicates an smtp session has been started by the HELO command
*
* @var bool
*/
protected $sess = false;
/**
* Indicates an smtp AUTH has been issued and authenticated
*
* @var bool
*/
protected $auth = false;
/**
* Indicates a MAIL command has been issued
*
* @var bool
*/
protected $mail = false;
/**
* Indicates one or more RCTP commands have been issued
*
* @var bool
*/
protected $rcpt = false;
/**
* Indicates that DATA has been issued and sent
*
* @var bool
*/
protected $data;
/**
* Whether or not send QUIT command
*
* @var bool
*/
protected $useCompleteQuit = true;
/**
* The first argument may be an array of all options. If so, it must include
* the 'host' and 'port' keys in order to ensure that all required values
* are present.
*
* @param string|array $host
* @param null|int $port
* @param null|array $config
* @throws Exception\InvalidArgumentException
*/
public function __construct($host = '127.0.0.1', $port = null, ?array $config = null)
{
// Did we receive a configuration array?
if (is_array($host)) {
// Merge config array with principal array, if provided
if (is_array($config)) {
$config = array_replace_recursive($host, $config);
} else {
$config = $host;
}
// Look for a host key; if none found, use default value
if (isset($config['host'])) {
$host = $config['host'];
} else {
$host = '127.0.0.1';
}
// Look for a port key; if none found, use default value
if (isset($config['port'])) {
$port = $config['port'];
} else {
$port = null;
}
}
// If we don't have a config array, initialize it
if (null === $config) {
$config = [];
}
if (isset($config['ssl'])) {
switch (strtolower($config['ssl'])) {
case 'tls':
$this->secure = 'tls';
break;
case 'ssl':
$this->transport = 'ssl';
$this->secure = 'ssl';
if ($port === null) {
$port = 465;
}
break;
case '':
// fall-through
case 'none':
break;
default:
throw new Exception\InvalidArgumentException($config['ssl'] . ' is unsupported SSL type');
}
}
if (array_key_exists('use_complete_quit', $config)) {
$this->setUseCompleteQuit($config['use_complete_quit']);
}
// If no port has been specified then check the master PHP ini file. Defaults to 25 if the ini setting is null.
if ($port === null) {
if (($port = ini_get('smtp_port')) == '') {
$port = 25;
}
}
if (array_key_exists('novalidatecert', $config)) {
$this->setNoValidateCert($config['novalidatecert']);
}
parent::__construct($host, $port);
}
/**
* Set whether or not send QUIT command
*
* @param bool $useCompleteQuit use complete quit
* @return bool
*/
public function setUseCompleteQuit($useCompleteQuit)
{
return $this->useCompleteQuit = (bool) $useCompleteQuit;
}
/**
* Read $data as lines terminated by "\n"
*
* @return Generator|string[]
*/
private static function chunkedReader(string $data, int $chunkSize = 4096): Generator
{
if (($fp = fopen("php://temp", "r+")) === false) {
throw new Exception\RuntimeException('cannot fopen');
}
if (fwrite($fp, $data) === false) {
throw new Exception\RuntimeException('cannot fwrite');
}
rewind($fp);
$line = null;
while (($buffer = fgets($fp, $chunkSize)) !== false) {
$line .= $buffer;
// This is optimization to avoid calling length() in a loop.
// We need to match a condition that is when:
// 1. maximum was read from fgets, which is $chunkSize-1
// 2. last byte of the buffer is not \n
//
// to access last byte of buffer, we can do
// - $buffer[strlen($buffer)-1]
// and when maximum is read from fgets, then:
// - strlen($buffer) === $chunkSize-1
// - strlen($buffer)-1 === $chunkSize-2
// which means this is also true:
// - $buffer[strlen($buffer)-1] === $buffer[$chunkSize-2]
//
// the null coalesce works, as string offset can never be null
$lastByte = $buffer[$chunkSize - 2] ?? null;
// partial read, continue loop to read again to complete the line
// compare \n first as that's usually false
if ($lastByte !== "\n" && $lastByte !== null) {
continue;
}
yield $line;
$line = null;
}
if ($line !== null) {
yield $line;
}
fclose($fp);
}
/**
* Whether or not send QUIT command
*
* @return bool
*/
public function useCompleteQuit()
{
return $this->useCompleteQuit;
}
/**
* Connect to the server with the parameters given in the constructor.
*
* @return bool
*/
public function connect()
{
$this->socket = $this->setupSocket(
$this->transport,
$this->host,
$this->port,
self::TIMEOUT_CONNECTION
);
return true;
}
/**
* Initiate HELO/EHLO sequence and set flag to indicate valid smtp session
*
* @param string $host The client hostname or IP address (default: 127.0.0.1)
* @throws Exception\RuntimeException
*/
public function helo($host = '127.0.0.1')
{
// Respect RFC 2821 and disallow HELO attempts if session is already initiated.
if ($this->sess === true) {
throw new Exception\RuntimeException('Cannot issue HELO to existing session');
}
// Validate client hostname
if (! $this->validHost->isValid($host)) {
throw new Exception\RuntimeException(implode(', ', $this->validHost->getMessages()));
}
// Initiate helo sequence
$this->_expect(220, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2
$this->ehlo($host);
// If a TLS session is required, commence negotiation
if ($this->secure == 'tls') {
$this->_send('STARTTLS');
$this->_expect(220, 180);
if (! stream_socket_enable_crypto($this->socket, true, $this->getCryptoMethod())) {
throw new Exception\RuntimeException('Unable to connect via TLS');
}
$this->ehlo($host);
}
$this->startSession();
$this->auth();
}
/**
* Returns the perceived session status
*
* @return bool
*/
public function hasSession()
{
return $this->sess;
}
/**
* Send EHLO or HELO depending on capabilities of smtp host
*
* @param string $host The client hostname or IP address (default: 127.0.0.1)
* @throws Exception\ExceptionInterface
*/
protected function ehlo($host)
{
// Support for older, less-compliant remote servers. Tries multiple attempts of EHLO or HELO.
try {
$this->_send('EHLO ' . $host);
$this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2
} catch (Exception\ExceptionInterface) {
$this->_send('HELO ' . $host);
$this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2
}
}
/**
* Issues MAIL command
*
* @param string $from Sender mailbox
* @throws Exception\RuntimeException
*/
public function mail($from)
{
if ($this->sess !== true) {
throw new Exception\RuntimeException('A valid session has not been started');
}
$this->_send('MAIL FROM:<' . $from . '>');
$this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2
// Set mail to true, clear recipients and any existing data flags as per 4.1.1.2 of RFC 2821
$this->mail = true;
$this->rcpt = false;
$this->data = false;
}
/**
* Issues RCPT command
*
* @param string $to Receiver(s) mailbox
* @throws Exception\RuntimeException
*/
public function rcpt($to)
{
if ($this->mail !== true) {
throw new Exception\RuntimeException('No sender reverse path has been supplied');
}
// Set rcpt to true, as per 4.1.1.3 of RFC 2821
$this->_send('RCPT TO:<' . $to . '>');
$this->_expect([250, 251], 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2
$this->rcpt = true;
}
/**
* Issues DATA command
*
* @param string $data
* @throws Exception\RuntimeException
*/
public function data($data)
{
// Ensure recipients have been set
if ($this->rcpt !== true) { // Per RFC 2821 3.3 (page 18)
throw new Exception\RuntimeException('No recipient forward path has been supplied');
}
$this->_send('DATA');
$this->_expect(354, 120); // Timeout set for 2 minutes as per RFC 2821 4.5.3.2
$reader = self::chunkedReader($data);
foreach ($reader as $line) {
$line = rtrim($line, "\r\n");
if (isset($line[0]) && $line[0] === '.') {
// Escape lines prefixed with a '.'
$line = '.' . $line;
}
if (strlen($line) > self::SMTP_LINE_LIMIT) {
// Long lines are "folded" by inserting "<CR><LF><SPACE>"
// https://tools.ietf.org/html/rfc5322#section-2.2.3
// Add "-1" to stay within limits,
// because Headers::FOLDING includes a byte for space character after \r\n
$chunks = chunk_split($line, self::SMTP_LINE_LIMIT - 1, Headers::FOLDING);
$line = substr($chunks, 0, -strlen(Headers::FOLDING));
}
$this->_send($line);
}
$this->_send('.');
$this->_expect(250, 600); // Timeout set for 10 minutes as per RFC 2821 4.5.3.2
$this->data = true;
}
/**
* Issues the RSET command end validates answer
*
* Can be used to restore a clean smtp communication state when a
* transaction has been cancelled or commencing a new transaction.
*/
public function rset()
{
$this->_send('RSET');
// MS ESMTP doesn't follow RFC, see https://zendframework.com/issues/browse/ZF-1377
$this->_expect([250, 220]);
$this->mail = false;
$this->rcpt = false;
$this->data = false;
}
/**
* Issues the NOOP command end validates answer
*
* Not used by Laminas\Mail, could be used to keep a connection alive or check if it is still open.
*/
public function noop()
{
$this->_send('NOOP');
$this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2
}
/**
* Issues the VRFY command end validates answer
*
* Not used by Laminas\Mail.
*
* @param string $user User Name or eMail to verify
*/
public function vrfy($user)
{
$this->_send('VRFY ' . $user);
$this->_expect([250, 251, 252], 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2
}
/**
* Issues the QUIT command and clears the current session
*/
public function quit()
{
if ($this->sess) {
$this->auth = false;
if ($this->useCompleteQuit()) {
$this->_send('QUIT');
$this->_expect(221, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2
}
$this->stopSession();
}
}
/**
* Default authentication method
*
* This default method is implemented by AUTH adapters to properly authenticate to a remote host.
*
* @throws Exception\RuntimeException
*/
public function auth()
{
if ($this->auth === true) {
throw new Exception\RuntimeException('Already authenticated for this session');
}
}
/**
* Closes connection
*/
public function disconnect()
{
$this->_disconnect();
}
/**
* Disconnect from remote host and free resource
*/
// @codingStandardsIgnoreLine PSR2.Methods.MethodDeclaration.Underscore
protected function _disconnect()
{
// Make sure the session gets closed
$this->quit();
parent::_disconnect();
}
/**
* Start mail session
*/
protected function startSession()
{
$this->sess = true;
}
/**
* Stop mail session
*/
protected function stopSession()
{
$this->sess = false;
}
}

View File

@@ -1,136 +0,0 @@
<?php
namespace Laminas\Mail\Protocol\Smtp\Auth;
use Laminas\Mail\Exception\InvalidArgumentException;
use Laminas\Mail\Protocol\Smtp;
use function array_replace_recursive;
use function base64_decode;
use function base64_encode;
use function hash_hmac;
use function is_array;
use function is_string;
/**
* Performs CRAM-MD5 authentication
*/
class Crammd5 extends Smtp
{
/** @var non-empty-string|null */
protected $username;
/** @var non-empty-string|null */
protected $password;
/**
* All parameters may be passed as an array to the first argument of the
* constructor. If so,
*
* @param string|array $host (Default: 127.0.0.1)
* @param null|int $port (Default: null)
* @param null|array $config Auth-specific parameters
*/
public function __construct($host = '127.0.0.1', $port = null, $config = null)
{
// Did we receive a configuration array?
$config = $config ?? [];
$origConfig = $config;
if (is_array($host)) {
// Merge config array with principal array, if provided
$config = array_replace_recursive($host, $config);
}
if (isset($config['username'])) {
$this->setUsername($config['username']);
}
if (isset($config['password'])) {
$this->setPassword($config['password']);
}
// Call parent with original arguments
parent::__construct($host, $port, $origConfig);
}
/**
* Performs CRAM-MD5 authentication with supplied credentials
*/
public function auth()
{
// Ensure AUTH has not already been initiated.
parent::auth();
$this->_send('AUTH CRAM-MD5');
$challenge = $this->_expect(334);
$challenge = base64_decode($challenge);
$digest = $this->hmacMd5($this->getPassword(), $challenge);
$this->_send(base64_encode($this->getUsername() . ' ' . $digest));
$this->_expect(235);
$this->auth = true;
}
/**
* Set value for username
*
* @param non-empty-string $username
* @return Crammd5
*/
public function setUsername($username)
{
$this->username = $username;
return $this;
}
/**
* Get username
*
* @return non-empty-string|null
*/
public function getUsername()
{
return $this->username;
}
/**
* Set value for password
*
* @param non-empty-string $password
* @return Crammd5
*/
public function setPassword($password)
{
$this->password = $password;
return $this;
}
/**
* Get password
*
* @return non-empty-string|null
*/
public function getPassword()
{
return $this->password;
}
/**
* Prepare CRAM-MD5 response to server's ticket
*
* @param non-empty-string $key Challenge key (usually password)
* @param non-empty-string $data Challenge data
* @param int $block Length of blocks (deprecated; unused)
* @return string
*/
protected function hmacMd5($key, $data, /** @deprecated */ $block = 64)
{
if (! is_string($key) || $key === '') {
throw new InvalidArgumentException('CramMD5 authentication requires a non-empty password');
}
if (! is_string($data) || $data === '') {
throw new InvalidArgumentException('CramMD5 authentication requires a non-empty challenge');
}
return hash_hmac('md5', $data, $key, false);
}
}

View File

@@ -1,121 +0,0 @@
<?php
namespace Laminas\Mail\Protocol\Smtp\Auth;
use Laminas\Mail\Protocol\Smtp;
use function array_replace_recursive;
use function base64_encode;
use function is_array;
/**
* Performs LOGIN authentication
*/
class Login extends Smtp
{
/**
* LOGIN username
*
* @var string
*/
protected $username;
/**
* LOGIN password
*
* @var string
*/
protected $password;
/**
* @param string $host (Default: 127.0.0.1)
* @param int $port (Default: null)
* @param array $config Auth-specific parameters
*/
public function __construct($host = '127.0.0.1', $port = null, $config = null)
{
// Did we receive a configuration array?
$origConfig = $config;
if (is_array($host)) {
// Merge config array with principal array, if provided
if (is_array($config)) {
$config = array_replace_recursive($host, $config);
} else {
$config = $host;
}
}
if (is_array($config)) {
if (isset($config['username'])) {
$this->setUsername($config['username']);
}
if (isset($config['password'])) {
$this->setPassword($config['password']);
}
}
// Call parent with original arguments
parent::__construct($host, $port, $origConfig);
}
/**
* Perform LOGIN authentication with supplied credentials
*/
public function auth()
{
// Ensure AUTH has not already been initiated.
parent::auth();
$this->_send('AUTH LOGIN');
$this->_expect(334);
$this->_send(base64_encode($this->getUsername()));
$this->_expect(334);
$this->_send(base64_encode($this->getPassword()));
$this->_expect(235);
$this->auth = true;
}
/**
* Set value for username
*
* @param string $username
* @return Login
*/
public function setUsername($username)
{
$this->username = $username;
return $this;
}
/**
* Get username
*
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* Set value for password
*
* @param string $password
* @return Login
*/
public function setPassword($password)
{
$this->password = $password;
return $this;
}
/**
* Get password
*
* @return string
*/
public function getPassword()
{
return $this->password;
}
}

View File

@@ -1,119 +0,0 @@
<?php
namespace Laminas\Mail\Protocol\Smtp\Auth;
use Laminas\Mail\Protocol\Smtp;
use function array_replace_recursive;
use function base64_encode;
use function is_array;
/**
* Performs PLAIN authentication
*/
class Plain extends Smtp
{
/**
* PLAIN username
*
* @var string
*/
protected $username;
/**
* PLAIN password
*
* @var string
*/
protected $password;
/**
* @param string $host (Default: 127.0.0.1)
* @param int $port (Default: null)
* @param array $config Auth-specific parameters
*/
public function __construct($host = '127.0.0.1', $port = null, $config = null)
{
// Did we receive a configuration array?
$origConfig = $config;
if (is_array($host)) {
// Merge config array with principal array, if provided
if (is_array($config)) {
$config = array_replace_recursive($host, $config);
} else {
$config = $host;
}
}
if (is_array($config)) {
if (isset($config['username'])) {
$this->setUsername($config['username']);
}
if (isset($config['password'])) {
$this->setPassword($config['password']);
}
}
// Call parent with original arguments
parent::__construct($host, $port, $origConfig);
}
/**
* Perform PLAIN authentication with supplied credentials
*/
public function auth()
{
// Ensure AUTH has not already been initiated.
parent::auth();
$this->_send('AUTH PLAIN');
$this->_expect(334);
$this->_send(base64_encode("\0" . $this->getUsername() . "\0" . $this->getPassword()));
$this->_expect(235);
$this->auth = true;
}
/**
* Set value for username
*
* @param string $username
* @return Plain
*/
public function setUsername($username)
{
$this->username = $username;
return $this;
}
/**
* Get username
*
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* Set value for password
*
* @param string $password
* @return Plain
*/
public function setPassword($password)
{
$this->password = $password;
return $this;
}
/**
* Get password
*
* @return string
*/
public function getPassword()
{
return $this->password;
}
}

View File

@@ -1,123 +0,0 @@
<?php
namespace Laminas\Mail\Protocol\Smtp\Auth;
use Laminas\Mail\Protocol\Smtp;
use Laminas\Mail\Protocol\Xoauth2\Xoauth2 as Xoauth2AuthEncoder;
use function array_replace_recursive;
use function is_array;
/**
* Performs Xoauth2 authentication
*
* @psalm-suppress PropertyNotSetInConstructor
*/
final class Xoauth2 extends Smtp
{
/**
* SMTP username
*
* @var string
*/
protected $username;
/**
* Xoauth2 access token
*
* @var string
*/
protected $accessToken;
/**
* @param string|array $host (Default: 127.0.0.1)
* @param int|null $port (Default: null)
* @param array|null $config Auth-specific parameters
*/
public function __construct($host = '127.0.0.1', $port = null, ?array $config = null)
{
// Did we receive a configuration array?
$origConfig = $config;
if (is_array($host)) {
// Merge config array with principal array, if provided
if (is_array($config)) {
$config = array_replace_recursive($host, $config);
} else {
$config = $host;
}
}
if (is_array($config)) {
if (isset($config['username'])) {
$this->setUsername((string) $config['username']);
}
if (isset($config['access_token'])) {
$this->setAccessToken((string) $config['access_token']);
}
}
// Call parent with original arguments
parent::__construct($host, $port, $origConfig);
}
/**
* Perform XOAUTH2 authentication with supplied credentials
*
* @return void
*/
public function auth()
{
// Ensure AUTH has not already been initiated.
parent::auth();
$this->_send('AUTH XOAUTH2');
$this->_expect('334');
$this->_send(Xoauth2AuthEncoder::encodeXoauth2Sasl($this->getUsername(), $this->getAccessToken()));
$this->_expect('235');
$this->auth = true;
}
/**
* Set value for username
*
* @param string $username
* @return Xoauth2
*/
public function setUsername($username)
{
$this->username = $username;
return $this;
}
/**
* Get username
*
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* Set value for access token
*
* @param string $token
* @return Xoauth2
*/
public function setAccessToken($token)
{
$this->accessToken = $token;
return $this;
}
/**
* Get access token
*
* @return string
*/
public function getAccessToken()
{
return $this->accessToken;
}
}

View File

@@ -1,121 +0,0 @@
<?php
namespace Laminas\Mail\Protocol;
use Laminas\ServiceManager\AbstractPluginManager;
use Laminas\ServiceManager\ConfigInterface;
use Laminas\ServiceManager\Exception\InvalidServiceException;
use Laminas\ServiceManager\Factory\InvokableFactory;
use function gettype;
use function is_object;
use function sprintf;
/**
* Plugin manager implementation for SMTP extensions.
*
* Enforces that SMTP extensions retrieved are instances of Smtp. Additionally,
* it registers a number of default extensions available.
*
* @link ConfigInterface
*
* @psalm-import-type FactoriesConfigurationType from ConfigInterface
*
* @extends AbstractPluginManager<Smtp>
* @final
*/
class SmtpPluginManager extends AbstractPluginManager
{
/**
* Service aliases
*
* @var array<array-key, class-string>
*/
protected $aliases = [
'crammd5' => Smtp\Auth\Crammd5::class,
'cramMd5' => Smtp\Auth\Crammd5::class,
'CramMd5' => Smtp\Auth\Crammd5::class,
'cramMD5' => Smtp\Auth\Crammd5::class,
'CramMD5' => Smtp\Auth\Crammd5::class,
'login' => Smtp\Auth\Login::class,
'Login' => Smtp\Auth\Login::class,
'plain' => Smtp\Auth\Plain::class,
'Plain' => Smtp\Auth\Plain::class,
'xoauth2' => Smtp\Auth\Xoauth2::class,
'Xoauth2' => Smtp\Auth\Xoauth2::class,
'smtp' => Smtp::class,
'Smtp' => Smtp::class,
'SMTP' => Smtp::class,
// Legacy Zend Framework aliases
'Zend\Mail\Protocol\Smtp\Auth\Crammd5' => Smtp\Auth\Crammd5::class,
'Zend\Mail\Protocol\Smtp\Auth\Login' => Smtp\Auth\Login::class,
'Zend\Mail\Protocol\Smtp\Auth\Plain' => Smtp\Auth\Plain::class,
'Zend\Mail\Protocol\Smtp' => Smtp::class,
// v2 normalized FQCNs
'zendmailprotocolsmtpauthcrammd5' => Smtp\Auth\Crammd5::class,
'zendmailprotocolsmtpauthlogin' => Smtp\Auth\Login::class,
'zendmailprotocolsmtpauthplain' => Smtp\Auth\Plain::class,
'zendmailprotocolsmtp' => Smtp::class,
'laminasmailprotocolsmtpauthcrammd5' => Smtp\Auth\Crammd5::class,
'laminasmailprotocolsmtpauthlogin' => Smtp\Auth\Login::class,
'laminasmailprotocolsmtpauthplain' => Smtp\Auth\Plain::class,
'laminasmailprotocolsmtp' => Smtp::class,
];
/**
* Service factories
*
* @var FactoriesConfigurationType
*/
protected $factories = [
Smtp\Auth\Crammd5::class => InvokableFactory::class,
Smtp\Auth\Login::class => InvokableFactory::class,
Smtp\Auth\Plain::class => InvokableFactory::class,
Smtp\Auth\Xoauth2::class => InvokableFactory::class,
Smtp::class => InvokableFactory::class,
];
/**
* Plugins must be an instance of the Smtp class
*
* @var class-string<Smtp>
*/
protected $instanceOf = Smtp::class;
/**
* Validate a retrieved plugin instance (v3).
*
* {@inheritDoc}
*/
public function validate(mixed $instance)
{
if (! $instance instanceof $this->instanceOf) {
throw new InvalidServiceException(sprintf(
'Plugin of type %s is invalid; must extend %s',
is_object($instance) ? $instance::class : gettype($instance),
$this->instanceOf
));
}
}
/**
* Validate a retrieved plugin instance (v2).
*
* @deprecated
*
* @param object $plugin
* @throws Exception\InvalidArgumentException
*/
public function validatePlugin(mixed $plugin)
{
try {
$this->validate($plugin);
} catch (InvalidServiceException $e) {
throw new Exception\InvalidArgumentException(
$e->getMessage(),
$e->getCode(),
$e
);
}
}
}

View File

@@ -1,57 +0,0 @@
<?php
namespace Laminas\Mail\Protocol;
// phpcs:ignore WebimpressCodingStandard.PHP.CorrectClassNameCase.Invalid
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
use Laminas\ServiceManager\ServiceManager;
/**
* @link ServiceManager
*
* @psalm-import-type ServiceManagerConfiguration from ServiceManager
*/
class SmtpPluginManagerFactory implements FactoryInterface
{
/**
* laminas-servicemanager v2 support for invocation options.
*
* @var array
* @psalm-var ServiceManagerConfiguration
*/
protected $creationOptions;
/**
* {@inheritDoc}
*
* @psalm-param ServiceManagerConfiguration $options
* @return SmtpPluginManager
*/
public function __invoke(ContainerInterface $container, $name, ?array $options = null)
{
return new SmtpPluginManager($container, $options ?: []);
}
/**
* {@inheritDoc}
*
* @return SmtpPluginManager
*/
public function createService(ServiceLocatorInterface $container, $name = null, $requestedName = null)
{
return $this($container, $requestedName ?: SmtpPluginManager::class, $this->creationOptions);
}
/**
* laminas-servicemanager v2 support for invocation options.
*
* @psalm-param ServiceManagerConfiguration $options
* @return void
*/
public function setCreationOptions(array $options)
{
$this->creationOptions = $options;
}
}

View File

@@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Mail\Protocol\Xoauth2;
use function base64_encode;
use function chr;
use function sprintf;
/**
* @internal
*/
final class Xoauth2
{
/**
* encodes accessToken and target mailbox to Xoauth2 SASL base64 encoded string
*/
public static function encodeXoauth2Sasl(string $targetMailbox, string $accessToken): string
{
return base64_encode(
sprintf(
"user=%s%sauth=Bearer %s%s%s",
$targetMailbox,
chr(0x01),
$accessToken,
chr(0x01),
chr(0x01)
)
);
}
}

View File

@@ -1,17 +0,0 @@
<?php
namespace Laminas\Mail;
class Storage
{
// maildir and IMAP flags, using IMAP names, where possible to be able to distinguish between IMAP
// system flags and other flags
public const FLAG_PASSED = 'Passed';
public const FLAG_SEEN = '\Seen';
public const FLAG_UNSEEN = '\Unseen';
public const FLAG_ANSWERED = '\Answered';
public const FLAG_FLAGGED = '\Flagged';
public const FLAG_DELETED = '\Deleted';
public const FLAG_DRAFT = '\Draft';
public const FLAG_RECENT = '\Recent';
}

View File

@@ -1,332 +0,0 @@
<?php
namespace Laminas\Mail\Storage;
use ArrayAccess;
use Countable;
use Laminas\Mail\Storage\Message;
use ReturnTypeWillChange;
use SeekableIterator;
use function str_starts_with;
use function strtolower;
use function substr;
abstract class AbstractStorage implements
ArrayAccess,
Countable,
SeekableIterator
{
/**
* class capabilities with default values
*
* @var array
*/
protected $has = [
'uniqueid' => true,
'delete' => false,
'create' => false,
'top' => false,
'fetchPart' => true,
'flags' => false,
];
/**
* current iteration position
*
* @var int
*/
protected $iterationPos = 0;
/**
* maximum iteration position (= message count)
*
* @var null|int
*/
protected $iterationMax;
/**
* used message class, change it in an extended class to extend the returned message class
*
* @var class-string<Message\MessageInterface>
*/
protected $messageClass = Message::class;
/**
* Getter for has-properties. The standard has properties
* are: hasFolder, hasUniqueid, hasDelete, hasCreate, hasTop
*
* The valid values for the has-properties are:
* - true if a feature is supported
* - false if a feature is not supported
* - null is it's not yet known or it can't be know if a feature is supported
*
* @param string $var property name
* @throws Exception\InvalidArgumentException
* @return null|bool supported or not
*/
public function __get($var)
{
if (str_starts_with($var, 'has')) {
$var = strtolower(substr($var, 3));
return $this->has[$var] ?? null;
}
throw new Exception\InvalidArgumentException($var . ' not found');
}
/**
* Get a full list of features supported by the specific mail lib and the server
*
* @return array list of features as array(feature_name => true|false[|null])
*/
public function getCapabilities()
{
return $this->has;
}
/**
* Count messages messages in current box/folder
*
* @return int number of messages
* @throws Exception\ExceptionInterface
*/
abstract public function countMessages();
/**
* Get a list of messages with number and size
*
* @param int $id number of message
* @return int|array size of given message of list with all messages as array(num => size)
*/
abstract public function getSize($id = 0);
/**
* Get a message with headers and body
*
* @param int $id number of message
* @return Message\MessageInterface
*/
abstract public function getMessage($id);
/**
* Get raw header of message or part
*
* @param int $id number of message
* @param null|array|string $part path to part or null for message header
* @param int $topLines include this many lines with header (after an empty line)
* @return string raw header
*/
abstract public function getRawHeader($id, $part = null, $topLines = 0);
/**
* Get raw content of message or part
*
* @param int $id number of message
* @param null|array|string $part path to part or null for message content
* @return string raw content
*/
abstract public function getRawContent($id, $part = null);
/**
* Create instance with parameters
*
* @param array $params mail reader specific parameters
* @throws Exception\ExceptionInterface
*/
abstract public function __construct($params);
/**
* Destructor calls close() and therefore closes the resource.
*/
public function __destruct()
{
$this->close();
}
/**
* Close resource for mail lib. If you need to control, when the resource
* is closed. Otherwise the destructor would call this.
*/
abstract public function close();
/**
* Keep the resource alive.
*/
abstract public function noop();
/**
* delete a message from current box/folder
*
* @param int $id message number
*/
abstract public function removeMessage($id);
/**
* get unique id for one or all messages
*
* if storage does not support unique ids it's the same as the message number
*
* @param int|null $id message number
* @return array|string message number for given message or all messages as array
* @throws Exception\ExceptionInterface
*/
abstract public function getUniqueId($id = null);
/**
* get a message number from a unique id
*
* I.e. if you have a webmailer that supports deleting messages you should use unique ids
* as parameter and use this method to translate it to message number right before calling removeMessage()
*
* @param string $id unique id
* @return int message number
* @throws Exception\ExceptionInterface
*/
abstract public function getNumberByUniqueId($id);
// interface implementations follows
/**
* Countable::count()
*
* @return int
*/
#[ReturnTypeWillChange]
public function count()
{
return $this->countMessages();
}
/**
* ArrayAccess::offsetExists()
*
* @param int $id
* @return bool
*/
#[ReturnTypeWillChange]
public function offsetExists($id)
{
try {
if ($this->getMessage($id)) {
return true;
}
} catch (Exception\ExceptionInterface) {
}
return false;
}
/**
* ArrayAccess::offsetGet()
*
* @param int $id
* @return Message message object
*/
#[ReturnTypeWillChange]
public function offsetGet($id)
{
return $this->getMessage($id);
}
/**
* ArrayAccess::offsetSet()
*
* @throws Exception\RuntimeException
*/
#[ReturnTypeWillChange]
public function offsetSet(mixed $id, mixed $value)
{
throw new Exception\RuntimeException('cannot write mail messages via array access');
}
/**
* ArrayAccess::offsetUnset()
*
* @param int $id
* @return bool success
*/
#[ReturnTypeWillChange]
public function offsetUnset($id)
{
return $this->removeMessage($id);
}
/**
* Iterator::rewind()
*
* Rewind always gets the new count from the storage. Thus if you use
* the interfaces and your scripts take long you should use reset()
* from time to time.
*/
#[ReturnTypeWillChange]
public function rewind()
{
$this->iterationMax = $this->countMessages();
$this->iterationPos = 1;
}
/**
* Iterator::current()
*
* @return Message current message
*/
#[ReturnTypeWillChange]
public function current()
{
return $this->getMessage($this->iterationPos);
}
/**
* Iterator::key()
*
* @return int id of current position
*/
#[ReturnTypeWillChange]
public function key()
{
return $this->iterationPos;
}
/**
* Iterator::next()
*/
#[ReturnTypeWillChange]
public function next()
{
++$this->iterationPos;
}
/**
* Iterator::valid()
*
* @return bool
*/
#[ReturnTypeWillChange]
public function valid()
{
if ($this->iterationMax === null) {
$this->iterationMax = $this->countMessages();
}
return $this->iterationPos && $this->iterationPos <= $this->iterationMax;
}
/**
* SeekableIterator::seek()
*
* @param int $pos
* @throws Exception\OutOfBoundsException
*/
#[ReturnTypeWillChange]
public function seek($pos)
{
if ($this->iterationMax === null) {
$this->iterationMax = $this->countMessages();
}
if ($pos > $this->iterationMax) {
throw new Exception\OutOfBoundsException('this position does not exist');
}
$this->iterationPos = $pos;
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Exception;
use Laminas\Mail\Exception\ExceptionInterface as MailException;
interface ExceptionInterface extends MailException
{
}

View File

@@ -1,12 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -1,12 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class OutOfBoundsException extends Exception\OutOfBoundsException implements ExceptionInterface
{
}

View File

@@ -1,12 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}

View File

@@ -1,202 +0,0 @@
<?php
namespace Laminas\Mail\Storage;
use RecursiveIterator;
use ReturnTypeWillChange;
use Stringable;
use function current;
use function key;
use function next;
use function reset;
class Folder implements RecursiveIterator, Stringable
{
/**
* global name (absolute name of folder)
*
* @var string
*/
protected $globalName;
/**
* create a new mail folder instance
*
* @param string $localName local name (name of folder in parent folder)
* @param string $globalName absolute name of folder
* @param bool $selectable if true folder holds messages, if false it's
* just a parent for subfolders (Default: true)
* @param array<string, Folder> $folders subfolders of
* folder array(localName => \Laminas\Mail\Storage\Folder folder)
*/
public function __construct(
protected $localName,
$globalName = '',
protected $selectable = true,
protected array $folders = []
) {
$this->globalName = $globalName ?: $localName;
}
/**
* implements RecursiveIterator::hasChildren()
*
* @return bool current element has children
*/
#[ReturnTypeWillChange]
public function hasChildren()
{
$current = $this->current();
return $current && $current instanceof self && ! $current->isLeaf();
}
/**
* implements RecursiveIterator::getChildren()
*
* @return Folder same as self::current()
*/
#[ReturnTypeWillChange]
public function getChildren()
{
return $this->current();
}
/**
* implements Iterator::valid()
*
* @return bool check if there's a current element
*/
#[ReturnTypeWillChange]
public function valid()
{
return key($this->folders) !== null;
}
/**
* implements Iterator::next()
*/
#[ReturnTypeWillChange]
public function next()
{
next($this->folders);
}
/**
* implements Iterator::key()
*
* @return string key/local name of current element
*/
#[ReturnTypeWillChange]
public function key()
{
return key($this->folders);
}
/**
* implements Iterator::current()
*
* @return Folder current folder
*/
#[ReturnTypeWillChange]
public function current()
{
return current($this->folders);
}
/**
* implements Iterator::rewind()
*/
#[ReturnTypeWillChange]
public function rewind()
{
reset($this->folders);
}
/**
* get subfolder named $name
*
* @param string $name wanted subfolder
* @throws Exception\InvalidArgumentException
* @return Folder folder named $folder
*/
public function __get($name)
{
if (! isset($this->folders[$name])) {
throw new Exception\InvalidArgumentException("no subfolder named $name");
}
return $this->folders[$name];
}
/**
* add or replace subfolder named $name
*
* @param string $name local name of subfolder
* @param Folder $folder instance for new subfolder
*/
public function __set($name, self $folder)
{
$this->folders[$name] = $folder;
}
/**
* remove subfolder named $name
*
* @param string $name local name of subfolder
*/
public function __unset($name)
{
unset($this->folders[$name]);
}
/**
* magic method for easy output of global name
*
* @return string global name of folder
*/
public function __toString(): string
{
return (string) $this->getGlobalName();
}
/**
* get local name
*
* @return string local name
*/
public function getLocalName()
{
return $this->localName;
}
/**
* get global name
*
* @return string global name
*/
public function getGlobalName()
{
return $this->globalName;
}
/**
* is this folder selectable?
*
* @return bool selectable
*/
public function isSelectable()
{
return $this->selectable;
}
/**
* check if folder has no subfolder
*
* @return bool true if no subfolders
*/
public function isLeaf()
{
return empty($this->folders);
}
}

View File

@@ -1,35 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Folder;
use Laminas\Mail\Storage\Exception\ExceptionInterface;
use Laminas\Mail\Storage\Folder;
interface FolderInterface
{
/**
* get root folder or given folder
*
* @param string $rootFolder get folder structure for given folder, else root
* @return Folder root or wanted folder
*/
public function getFolders($rootFolder = null);
/**
* select given folder
*
* folder must be selectable!
*
* @param Folder|string $globalName global name of folder or instance for subfolder
* @throws ExceptionInterface
*/
public function selectFolder($globalName);
/**
* get Laminas\Mail\Storage\Folder instance for current folder
*
* @return string instance of current folder
* @throws ExceptionInterface
*/
public function getCurrentFolder();
}

View File

@@ -1,247 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Folder;
use Laminas\Mail\Storage;
use Laminas\Mail\Storage\Exception;
use Laminas\Mail\Storage\Exception\InvalidArgumentException;
use Laminas\Mail\Storage\Folder;
use Laminas\Mail\Storage\ParamsNormalizer;
use Laminas\Stdlib\ErrorHandler;
use function array_pop;
use function array_push;
use function closedir;
use function explode;
use function is_dir;
use function opendir;
use function readdir;
use function rtrim;
use function sort;
use function str_contains;
use function str_starts_with;
use function strlen;
use function substr;
use function trim;
use const DIRECTORY_SEPARATOR;
use const E_WARNING;
class Maildir extends Storage\Maildir implements FolderInterface
{
/**
* root folder for folder structure
*
* @var Storage\Folder
*/
protected $rootFolder;
/**
* rootdir of folder structure
*
* @var string
*/
protected $rootdir;
/**
* name of current folder
*
* @var string
*/
protected $currentFolder;
/**
* delim char for subfolders
*
* @var string
*/
protected $delim;
/**
* Create instance with parameters
*
* Supported parameters are:
*
* - dirname rootdir of maildir structure
* - delim delim char for folder structure, default is '.'
* - folder initial selected folder, default is 'INBOX'
*
* @param object|array $params mail reader specific parameters
* @throws Exception\InvalidArgumentException
*/
public function __construct($params)
{
$params = ParamsNormalizer::normalizeParams($params);
if (! isset($params['dirname'])) {
throw new Exception\InvalidArgumentException('no dirname provided in params');
}
$dirname = (string) $params['dirname'];
if (! is_dir($dirname)) {
throw new Exception\InvalidArgumentException('$dirname provided in params is not a directory');
}
$this->rootdir = rtrim($dirname, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$delim = $params['delim'] ?? '.';
$this->delim = (string) $delim;
$folder = $params['folder'] ?? 'INBOX';
$this->buildFolderTree();
$this->selectFolder((string) $folder);
$this->has['top'] = true;
$this->has['flags'] = true;
}
/**
* find all subfolders and mbox files for folder structure
*
* Result is save in Storage\Folder instances with the root in $this->rootFolder.
* $parentFolder and $parentGlobalName are only used internally for recursion.
*
* @throws Exception\RuntimeException
*/
protected function buildFolderTree()
{
$this->rootFolder = new Storage\Folder('/', '/', false);
$this->rootFolder->INBOX = new Storage\Folder('INBOX', 'INBOX', true);
ErrorHandler::start(E_WARNING);
$dh = opendir($this->rootdir);
$error = ErrorHandler::stop();
if (! $dh) {
throw new Exception\RuntimeException("can't read folders in maildir", 0, $error);
}
$dirs = [];
while (($entry = readdir($dh)) !== false) {
// maildir++ defines folders must start with .
if ($entry[0] != '.' || $entry == '.' || $entry == '..') {
continue;
}
if ($this->isMaildir($this->rootdir . $entry)) {
$dirs[] = $entry;
}
}
closedir($dh);
sort($dirs);
$stack = [null];
$folderStack = [null];
$parentFolder = $this->rootFolder;
$parent = '.';
foreach ($dirs as $dir) {
do {
if (str_starts_with($dir, $parent)) {
$local = substr($dir, strlen((string) $parent));
if (str_contains($local, $this->delim)) {
throw new Exception\RuntimeException('error while reading maildir');
}
array_push($stack, $parent);
$parent = $dir . $this->delim;
$folder = new Storage\Folder($local, substr($dir, 1), true);
$parentFolder->$local = $folder;
array_push($folderStack, $parentFolder);
$parentFolder = $folder;
break;
} elseif ($stack) {
$parent = array_pop($stack);
$parentFolder = array_pop($folderStack);
}
} while ($stack);
if (! $stack) {
throw new Exception\RuntimeException('error while reading maildir');
}
}
}
/**
* get root folder or given folder
*
* @param string $rootFolder get folder structure for given folder, else root
* @throws InvalidArgumentException
* @return Folder root or wanted folder
*/
public function getFolders($rootFolder = null)
{
if (! $rootFolder || $rootFolder == 'INBOX') {
return $this->rootFolder;
}
// rootdir is same as INBOX in maildir
if (str_starts_with($rootFolder, 'INBOX' . $this->delim)) {
$rootFolder = substr($rootFolder, 6);
}
$currentFolder = $this->rootFolder;
$subname = trim($rootFolder, $this->delim);
while ($currentFolder) {
if (str_contains($subname, $this->delim)) {
[$entry, $subname] = explode($this->delim, $subname, 2);
} else {
$entry = $subname;
$subname = null;
}
$currentFolder = $currentFolder->$entry;
if (! $subname) {
break;
}
}
if ($currentFolder->getGlobalName() != rtrim($rootFolder, $this->delim)) {
throw new Exception\InvalidArgumentException("folder $rootFolder not found");
}
return $currentFolder;
}
/**
* select given folder
*
* folder must be selectable!
*
* @param Storage\Folder|string $globalName global name of folder or
* instance for subfolder
* @throws Exception\RuntimeException
*/
public function selectFolder($globalName)
{
$this->currentFolder = (string) $globalName;
// getting folder from folder tree for validation
$folder = $this->getFolders($this->currentFolder);
try {
$this->openMaildir($this->rootdir . '.' . $folder->getGlobalName());
} catch (Exception\ExceptionInterface $e) {
// check what went wrong
if (! $folder->isSelectable()) {
throw new Exception\RuntimeException("{$this->currentFolder} is not selectable", 0, $e);
}
// seems like file has vanished; rebuilding folder tree - but it's still an exception
$this->buildFolderTree();
throw new Exception\RuntimeException(
'seems like the maildir has vanished; I have rebuilt the folder tree; '
. 'search for another folder and try again',
0,
$e
);
}
}
/**
* get Storage\Folder instance for current folder
*
* @return string instance of current folder
*/
public function getCurrentFolder()
{
return $this->currentFolder;
}
}

View File

@@ -1,237 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Folder;
use Laminas\Mail\Storage;
use Laminas\Mail\Storage\Exception;
use Laminas\Mail\Storage\ParamsNormalizer;
use Laminas\Stdlib\ErrorHandler;
use function array_merge;
use function closedir;
use function explode;
use function is_dir;
use function is_file;
use function opendir;
use function readdir;
use function rtrim;
use function sprintf;
use function str_contains;
use function trim;
use const DIRECTORY_SEPARATOR;
use const E_WARNING;
class Mbox extends Storage\Mbox implements FolderInterface
{
/**
* Storage\Folder root folder for folder structure
*
* @var Storage\Folder
*/
protected $rootFolder;
/**
* rootdir of folder structure
*
* @var string
*/
protected $rootdir;
/**
* name of current folder
*
* @var string
*/
protected $currentFolder;
/**
* Create instance with parameters
*
* Disallowed parameters are:
* - filename use \Laminas\Mail\Storage\Mbox for a single file
*
* Supported parameters are:
*
* - dirname rootdir of mbox structure
* - folder initial selected folder, default is 'INBOX'
*
* @param array|object $params Array, iterable object, or stdClass object
* with reader specific parameters
* @throws Exception\InvalidArgumentException
*/
public function __construct($params)
{
$params = ParamsNormalizer::normalizeParams($params);
if (isset($params['filename'])) {
throw new Exception\InvalidArgumentException(sprintf('use %s for a single file', Storage\Mbox::class));
}
if (! isset($params['dirname'])) {
throw new Exception\InvalidArgumentException('no dirname provided in params');
}
$dirname = (string) $params['dirname'];
if (! is_dir($dirname)) {
throw new Exception\InvalidArgumentException('$dirname provided in params is not a directory');
}
$this->rootdir = rtrim($dirname, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$folder = $params['folder'] ?? 'INBOX';
$this->buildFolderTree($this->rootdir);
$this->selectFolder((string) $folder);
$this->has['top'] = true;
$this->has['uniqueid'] = false;
}
/**
* find all subfolders and mbox files for folder structure
*
* Result is save in Storage\Folder instances with the root in $this->rootFolder.
* $parentFolder and $parentGlobalName are only used internally for recursion.
*
* @param string $currentDir call with root dir, also used for recursion.
* @param Storage\Folder|null $parentFolder used for recursion
* @param string $parentGlobalName used for recursion
* @throws Exception\InvalidArgumentException
*/
protected function buildFolderTree($currentDir, $parentFolder = null, $parentGlobalName = '')
{
if (! $parentFolder) {
$this->rootFolder = new Storage\Folder('/', '/', false);
$parentFolder = $this->rootFolder;
}
ErrorHandler::start(E_WARNING);
$dh = opendir($currentDir);
ErrorHandler::stop();
if (! $dh) {
throw new Exception\InvalidArgumentException("can't read dir $currentDir");
}
while (($entry = readdir($dh)) !== false) {
// ignore hidden files for mbox
if ($entry[0] == '.') {
continue;
}
$absoluteEntry = $currentDir . $entry;
$globalName = $parentGlobalName . DIRECTORY_SEPARATOR . $entry;
if (is_file($absoluteEntry) && $this->isMboxFile($absoluteEntry)) {
$parentFolder->$entry = new Storage\Folder($entry, $globalName);
continue;
}
if (! is_dir($absoluteEntry)) { /* || $entry == '.' || $entry == '..' */
continue;
}
$folder = new Storage\Folder($entry, $globalName, false);
$parentFolder->$entry = $folder;
$this->buildFolderTree($absoluteEntry . DIRECTORY_SEPARATOR, $folder, $globalName);
}
closedir($dh);
}
/**
* get root folder or given folder
*
* @param string $rootFolder get folder structure for given folder, else root
* @return Storage\Folder root or wanted folder
* @throws Exception\InvalidArgumentException
*/
public function getFolders($rootFolder = null)
{
if (! $rootFolder) {
return $this->rootFolder;
}
$currentFolder = $this->rootFolder;
$subname = trim($rootFolder, DIRECTORY_SEPARATOR);
while ($currentFolder) {
if (str_contains($subname, DIRECTORY_SEPARATOR)) {
[$entry, $subname] = explode(DIRECTORY_SEPARATOR, $subname, 2);
} else {
$entry = $subname;
$subname = null;
}
$currentFolder = $currentFolder->$entry;
if (! $subname) {
break;
}
}
if ($currentFolder->getGlobalName() != DIRECTORY_SEPARATOR . trim($rootFolder, DIRECTORY_SEPARATOR)) {
throw new Exception\InvalidArgumentException("folder $rootFolder not found");
}
return $currentFolder;
}
/**
* select given folder
*
* folder must be selectable!
*
* @param Storage\Folder|string $globalName global name of folder or
* instance for subfolder
* @throws Exception\RuntimeException
*/
public function selectFolder($globalName)
{
$this->currentFolder = (string) $globalName;
// getting folder from folder tree for validation
$folder = $this->getFolders($this->currentFolder);
try {
$this->openMboxFile($this->rootdir . $folder->getGlobalName());
} catch (Exception\ExceptionInterface $e) {
// check what went wrong
if (! $folder->isSelectable()) {
throw new Exception\RuntimeException("{$this->currentFolder} is not selectable", 0, $e);
}
// seems like file has vanished; rebuilding folder tree - but it's still an exception
$this->buildFolderTree($this->rootdir);
throw new Exception\RuntimeException(
'seems like the mbox file has vanished; I have rebuilt the folder tree; '
. 'search for another folder and try again',
0,
$e
);
}
}
/**
* get Storage\Folder instance for current folder
*
* @return string instance of current folder
* @throws Exception\ExceptionInterface
*/
public function getCurrentFolder()
{
return $this->currentFolder;
}
/**
* magic method for serialize()
*
* with this method you can cache the mbox class
*
* @return array name of variables
*/
public function __sleep()
{
return array_merge(parent::__sleep(), ['currentFolder', 'rootFolder', 'rootdir']);
}
/**
* magic method for unserialize(), with this method you can cache the mbox class
*/
public function __wakeup()
{
// if cache is stall selectFolder() rebuilds the tree on error
parent::__wakeup();
}
}

View File

@@ -1,571 +0,0 @@
<?php
namespace Laminas\Mail\Storage;
use Laminas\Mail;
use Laminas\Mail\Protocol;
use function array_key_exists;
use function array_pop;
use function array_push;
use function count;
use function in_array;
use function is_string;
use function ksort;
use function str_starts_with;
use function strrpos;
use function substr;
use const INF;
use const SORT_STRING;
class Imap extends AbstractStorage implements Folder\FolderInterface, Writable\WritableInterface
{
// TODO: with an internal cache we could optimize this class, or create an extra class with
// such optimizations. Especially the various fetch calls could be combined to one cache call
/**
* protocol handler
*
* @var null|Protocol\Imap
*/
protected $protocol;
/**
* name of current folder
*
* @var string
*/
protected $currentFolder = '';
/**
* IMAP folder delimiter character
*
* @var null|string
*/
protected $delimiter;
/**
* IMAP flags to constants translation
*
* @var array
*/
protected static $knownFlags = [
'\Passed' => Mail\Storage::FLAG_PASSED,
'\Answered' => Mail\Storage::FLAG_ANSWERED,
'\Seen' => Mail\Storage::FLAG_SEEN,
'\Unseen' => Mail\Storage::FLAG_UNSEEN,
'\Deleted' => Mail\Storage::FLAG_DELETED,
'\Draft' => Mail\Storage::FLAG_DRAFT,
'\Flagged' => Mail\Storage::FLAG_FLAGGED,
];
/**
* IMAP flags to search criteria
*
* @var array
*/
protected static $searchFlags = [
'\Recent' => 'RECENT',
'\Answered' => 'ANSWERED',
'\Seen' => 'SEEN',
'\Unseen' => 'UNSEEN',
'\Deleted' => 'DELETED',
'\Draft' => 'DRAFT',
'\Flagged' => 'FLAGGED',
];
/**
* Count messages all messages in current box
*
* @param null $flags
* @throws Exception\RuntimeException
* @throws Protocol\Exception\RuntimeException
* @return int number of messages
*/
public function countMessages($flags = null)
{
if (! $this->currentFolder) {
throw new Exception\RuntimeException('No selected folder to count');
}
if ($flags === null) {
return count($this->protocol->search(['ALL']));
}
$params = [];
foreach ((array) $flags as $flag) {
if (isset(static::$searchFlags[$flag])) {
$params[] = static::$searchFlags[$flag];
} else {
$params[] = 'KEYWORD';
$params[] = $this->protocol->escapeString($flag);
}
}
return count($this->protocol->search($params));
}
/**
* get a list of messages with number and size
*
* @param int $id number of message
* @return int|array size of given message of list with all messages as [num => size]
* @throws Protocol\Exception\RuntimeException
*/
public function getSize($id = 0)
{
if ($id) {
return $this->protocol->fetch('RFC822.SIZE', $id);
}
return $this->protocol->fetch('RFC822.SIZE', 1, INF);
}
/**
* Fetch a message
*
* @param int $id number of message
* @return Message
* @throws Protocol\Exception\RuntimeException
*/
public function getMessage($id)
{
$data = $this->protocol->fetch(['FLAGS', 'RFC822.HEADER'], $id);
$header = $data['RFC822.HEADER'];
$flags = [];
foreach ($data['FLAGS'] as $flag) {
$flags[] = static::$knownFlags[$flag] ?? $flag;
}
return new $this->messageClass(['handler' => $this, 'id' => $id, 'headers' => $header, 'flags' => $flags]);
}
/**
* Get raw header of message or part
*
* @param int $id number of message
* @param null|array|string $part path to part or null for message header
* @param int $topLines include this many lines with header (after an empty line)
* @return string raw header
* @throws Exception\RuntimeException
* @throws Protocol\Exception\RuntimeException
*/
public function getRawHeader($id, $part = null, $topLines = 0)
{
if ($part !== null) {
// TODO: implement
throw new Exception\RuntimeException('not implemented');
}
// TODO: toplines
return $this->protocol->fetch('RFC822.HEADER', $id);
}
/**
* Get raw content of message or part
*
* @param int $id number of message
* @param null|array|string $part path to part or null for message content
* @return string raw content
* @throws Protocol\Exception\RuntimeException
* @throws Exception\RuntimeException
*/
public function getRawContent($id, $part = null)
{
if ($part !== null) {
// TODO: implement
throw new Exception\RuntimeException('not implemented');
}
return $this->protocol->fetch('RFC822.TEXT', $id);
}
/**
* create instance with parameters
*
* Supported parameters are
*
* - user username
* - host hostname or ip address of IMAP server [optional, default = 'localhost']
* - password password for user 'username' [optional, default = '']
* - port port for IMAP server [optional, default = 110]
* - ssl 'SSL' or 'TLS' for secure sockets
* - folder select this folder [optional, default = 'INBOX']
*
* @param array|object|Protocol\Imap $params mail reader specific
* parameters or configured Imap protocol object
* @throws Exception\RuntimeException
* @throws Exception\InvalidArgumentException
* @throws Protocol\Exception\RuntimeException
*/
public function __construct($params)
{
$this->has['flags'] = true;
if ($params instanceof Protocol\Imap) {
$this->protocol = $params;
try {
$this->selectFolder('INBOX');
} catch (Exception\ExceptionInterface $e) {
throw new Exception\RuntimeException('cannot select INBOX, is this a valid transport?', 0, $e);
}
return;
}
$params = ParamsNormalizer::normalizeParams($params);
if (! isset($params['user'])) {
throw new Exception\InvalidArgumentException('need at least user in params');
}
$host = $params['host'] ?? 'localhost';
$password = $params['password'] ?? '';
$port = $params['port'] ?? null;
$ssl = $params['ssl'] ?? false;
$folder = $params['folder'] ?? 'INBOX';
if (null !== $port) {
$port = (int) $port;
}
if (! is_string($ssl)) {
$ssl = (bool) $ssl;
}
$this->protocol = new Protocol\Imap();
if (array_key_exists('novalidatecert', $params)) {
$this->protocol->setNoValidateCert((bool) $params['novalidatecert']);
}
$this->protocol->connect((string) $host, $port, $ssl);
if (! $this->protocol->login((string) $params['user'], (string) $password)) {
throw new Exception\RuntimeException('cannot login, user or password wrong');
}
$this->selectFolder((string) $folder);
}
/**
* Close resource for mail lib.
*
* If you need to control, when the resource is closed. Otherwise the
* destructor would call this.
*/
public function close()
{
$this->currentFolder = '';
$this->protocol->logout();
}
/**
* Keep the server busy.
*
* @throws Exception\RuntimeException
*/
public function noop()
{
if (! $this->protocol->noop()) {
throw new Exception\RuntimeException('could not do nothing');
}
}
/**
* Remove a message from server.
*
* If you're doing that from a web environment you should be careful and
* use a uniqueid as parameter if possible to identify the message.
*
* @param int $id number of message
* @throws Exception\RuntimeException
*/
public function removeMessage($id)
{
if (! $this->protocol->store([Mail\Storage::FLAG_DELETED], $id, null, '+')) {
throw new Exception\RuntimeException('cannot set deleted flag');
}
// TODO: expunge here or at close? we can handle an error here better and are more fail safe
if (! $this->protocol->expunge()) {
throw new Exception\RuntimeException('message marked as deleted, but could not expunge');
}
}
/**
* get unique id for one or all messages
*
* if storage does not support unique ids it's the same as the message
* number.
*
* @param int|null $id message number
* @return array|string message number for given message or all messages as array
* @throws Protocol\Exception\RuntimeException
*/
public function getUniqueId($id = null)
{
if ($id) {
return $this->protocol->fetch('UID', $id);
}
return $this->protocol->fetch('UID', 1, INF);
}
/**
* get a message number from a unique id
*
* I.e. if you have a webmailer that supports deleting messages you should
* use unique ids as parameter and use this method to translate it to
* message number right before calling removeMessage()
*
* @param string $id unique id
* @throws Exception\InvalidArgumentException
* @return int message number
*/
public function getNumberByUniqueId($id)
{
// TODO: use search to find number directly
$ids = $this->getUniqueId();
foreach ($ids as $k => $v) {
if ($v == $id) {
return $k;
}
}
throw new Exception\InvalidArgumentException('unique id not found');
}
/**
* get root folder or given folder
*
* @param string $rootFolder get folder structure for given folder, else root
* @throws Exception\RuntimeException
* @throws Exception\InvalidArgumentException
* @throws Protocol\Exception\RuntimeException
* @return Folder root or wanted folder
*/
public function getFolders($rootFolder = null)
{
$folders = $this->protocol->listMailbox((string) $rootFolder);
if (! $folders) {
throw new Exception\InvalidArgumentException('folder not found');
}
ksort($folders, SORT_STRING);
$root = new Folder('/', '/', false);
$stack = [null];
$folderStack = [null];
$parentFolder = $root;
$parent = '';
foreach ($folders as $globalName => $data) {
do {
if (! $parent || str_starts_with($globalName, ! is_string($parent) ? (string) $parent : $parent)) {
$pos = strrpos($globalName, (string) $data['delim']);
if ($pos === false) {
$localName = $globalName;
} else {
$localName = substr($globalName, $pos + 1);
}
$selectable = ! $data['flags'] || ! in_array('\\Noselect', $data['flags']);
array_push($stack, $parent);
$parent = $globalName . $data['delim'];
$folder = new Folder($localName, $globalName, $selectable);
$parentFolder->$localName = $folder;
array_push($folderStack, $parentFolder);
$parentFolder = $folder;
$this->delimiter = $data['delim'];
break;
} elseif ($stack) {
$parent = array_pop($stack);
$parentFolder = array_pop($folderStack);
}
} while ($stack);
if (! $stack) {
throw new Exception\RuntimeException('error while constructing folder tree');
}
}
return $root;
}
/**
* select given folder
*
* folder must be selectable!
*
* @param Folder|string $globalName global name of folder or instance for subfolder
* @throws Exception\RuntimeException
* @throws Protocol\Exception\RuntimeException
*/
public function selectFolder($globalName)
{
$this->currentFolder = (string) $globalName;
if (! $this->protocol->select($this->currentFolder)) {
$this->currentFolder = '';
throw new Exception\RuntimeException('cannot change folder, maybe it does not exist');
}
}
/**
* get Folder instance for current folder
*
* @return string instance of current folder
*/
public function getCurrentFolder()
{
return $this->currentFolder;
}
/**
* create a new folder
*
* This method also creates parent folders if necessary. Some mail storages
* may restrict, which folder may be used as parent or which chars may be
* used in the folder name
*
* @param string $name global name of folder, local name if $parentFolder
* is set
* @param string|Folder $parentFolder parent folder for new folder, else
* root folder is parent
* @throws Exception\RuntimeException
*/
public function createFolder($name, $parentFolder = null)
{
// TODO: we assume / as the hierarchy delim - need to get that from the folder class!
if ($parentFolder instanceof Folder) {
$folder = $parentFolder->getGlobalName() . '/' . $name;
} elseif ($parentFolder !== null) {
$folder = $parentFolder . '/' . $name;
} else {
$folder = $name;
}
if (! $this->protocol->create($folder)) {
throw new Exception\RuntimeException('cannot create folder');
}
}
/**
* remove a folder
*
* @param string|Folder $name name or instance of folder
* @throws Exception\RuntimeException
*/
public function removeFolder($name)
{
if ($name instanceof Folder) {
$name = $name->getGlobalName();
}
if (! $this->protocol->delete($name)) {
throw new Exception\RuntimeException('cannot delete folder');
}
}
/**
* rename and/or move folder
*
* The new name has the same restrictions as in createFolder()
*
* @param string|Folder $oldName name or instance of folder
* @param string $newName new global name of folder
* @throws Exception\RuntimeException
*/
public function renameFolder($oldName, $newName)
{
if ($oldName instanceof Folder) {
$oldName = $oldName->getGlobalName();
}
if (! $this->protocol->rename($oldName, $newName)) {
throw new Exception\RuntimeException('cannot rename folder');
}
}
/**
* append a new message to mail storage
*
* @param string $message message as string or instance of message class
* @param null|string|Folder $folder folder for new message, else current
* folder is taken
* @param null|array $flags set flags for new message, else a default set
* is used
* @throws Exception\RuntimeException
*/
public function appendMessage($message, $folder = null, $flags = null)
{
if ($folder === null) {
$folder = $this->currentFolder;
}
if ($flags === null) {
$flags = [Mail\Storage::FLAG_SEEN];
}
// TODO: handle class instances for $message
if (! $this->protocol->append($folder, $message, $flags)) {
throw new Exception\RuntimeException(
'cannot create message, please check if the folder exists and your flags'
);
}
}
/**
* copy an existing message
*
* @param int $id number of message
* @param string|Folder $folder name or instance of target folder
* @throws Exception\RuntimeException
*/
public function copyMessage($id, $folder)
{
if (! $this->protocol->copy($folder, $id)) {
throw new Exception\RuntimeException('cannot copy message, does the folder exist?');
}
}
/**
* move an existing message
*
* NOTE: IMAP has no native move command, thus it's emulated with copy and delete
*
* @param int $id number of message
* @param string|Folder $folder name or instance of target folder
* @throws Exception\RuntimeException
*/
public function moveMessage($id, $folder)
{
$this->copyMessage($id, $folder);
$this->removeMessage($id);
}
/**
* set flags for message
*
* NOTE: this method can't set the recent flag.
*
* @param int $id number of message
* @param array $flags new flags for message
* @throws Exception\RuntimeException
*/
public function setFlags($id, $flags)
{
if (! $this->protocol->store($flags, $id)) {
throw new Exception\RuntimeException(
'cannot set flags, have you tried to set the recent flag or special chars?'
);
}
}
/**
* get IMAP delimiter
*
* @return string|null
*/
public function delimiter()
{
if (! isset($this->delimiter)) {
$this->getFolders();
}
return $this->delimiter;
}
}

View File

@@ -1,460 +0,0 @@
<?php
namespace Laminas\Mail\Storage;
use Laminas\Mail;
use Laminas\Mail\Storage\Exception\ExceptionInterface;
use Laminas\Mail\Storage\Message\File;
use Laminas\Stdlib\ErrorHandler;
use function array_flip;
use function closedir;
use function count;
use function ctype_digit;
use function explode;
use function fclose;
use function feof;
use function fgets;
use function file_exists;
use function filesize;
use function fopen;
use function is_array;
use function is_dir;
use function is_file;
use function is_subclass_of;
use function opendir;
use function readdir;
use function sprintf;
use function str_contains;
use function strcmp;
use function stream_get_contents;
use function strlen;
use function substr;
use function trim;
use function usort;
use const E_WARNING;
class Maildir extends AbstractStorage
{
/**
* used message class, change it in an extended class to extend the returned message class
*
* @var class-string<Mail\Storage\Message\MessageInterface>
*/
protected $messageClass = File::class;
/**
* data of found message files in maildir dir
*
* @var array
*/
protected $files = [];
/**
* known flag chars in filenames
*
* This list has to be in alphabetical order for setFlags()
*
* @var array
*/
protected static $knownFlags = [
'D' => Mail\Storage::FLAG_DRAFT,
'F' => Mail\Storage::FLAG_FLAGGED,
'P' => Mail\Storage::FLAG_PASSED,
'R' => Mail\Storage::FLAG_ANSWERED,
'S' => Mail\Storage::FLAG_SEEN,
'T' => Mail\Storage::FLAG_DELETED,
];
// TODO: getFlags($id) for fast access if headers are not needed (i.e. just setting flags)?
/**
* Count messages all messages in current box
*
* @param mixed $flags
* @return int number of messages
*/
public function countMessages($flags = null)
{
if ($flags === null) {
return count($this->files);
}
$count = 0;
if (! is_array($flags)) {
foreach ($this->files as $file) {
if (isset($file['flaglookup'][$flags])) {
++$count;
}
}
return $count;
}
$flags = array_flip($flags);
foreach ($this->files as $file) {
foreach ($flags as $flag => $v) {
if (! isset($file['flaglookup'][$flag])) {
continue 2;
}
}
++$count;
}
return $count;
}
/**
* Get one or all fields from file structure. Also checks if message is valid
*
* @param int $id message number
* @param string|null $field wanted field
* @throws Exception\InvalidArgumentException
* @return string|array wanted field or all fields as array
*/
protected function getFileData($id, $field = null)
{
if (! isset($this->files[$id - 1])) {
throw new Exception\InvalidArgumentException('id does not exist');
}
if (! $field) {
return $this->files[$id - 1];
}
if (! isset($this->files[$id - 1][$field])) {
throw new Exception\InvalidArgumentException('field does not exist');
}
return $this->files[$id - 1][$field];
}
/**
* Get a list of messages with number and size
*
* @param int|null $id number of message or null for all messages
* @return int|array size of given message of list with all messages as array(num => size)
*/
public function getSize($id = null)
{
if ($id !== null) {
$filedata = $this->getFileData($id);
return $filedata['size'] ?? filesize($filedata['filename']);
}
$result = [];
foreach ($this->files as $num => $data) {
$result[$num + 1] = $data['size'] ?? filesize($data['filename']);
}
return $result;
}
/**
* Fetch a message
*
* @param int $id number of message
* @return File
* @throws ExceptionInterface
*/
public function getMessage($id)
{
// TODO that's ugly, would be better to let the message class decide
if (
trim($this->messageClass, '\\') === File::class
|| is_subclass_of($this->messageClass, File::class)
) {
return new $this->messageClass([
'file' => $this->getFileData($id, 'filename'),
'flags' => $this->getFileData($id, 'flags'),
]);
}
return new $this->messageClass([
'handler' => $this,
'id' => $id,
'headers' => $this->getRawHeader($id),
'flags' => $this->getFileData($id, 'flags'),
]);
}
/**
* Get raw header of message or part
*
* @param int $id number of message
* @param null|array|string $part path to part or null for message header
* @param int $topLines include this many lines with header (after an empty line)
* @throws Exception\RuntimeException
* @return string raw header
*/
public function getRawHeader($id, $part = null, $topLines = 0)
{
if ($part !== null) {
// TODO: implement
throw new Exception\RuntimeException('not implemented');
}
$fh = fopen($this->getFileData($id, 'filename'), 'r');
$content = '';
while (! feof($fh)) {
$line = fgets($fh);
if (! trim($line)) {
break;
}
$content .= $line;
}
fclose($fh);
return $content;
}
/**
* Get raw content of message or part
*
* @param int $id number of message
* @param null|array|string $part path to part or null for message content
* @throws Exception\RuntimeException
* @return string raw content
*/
public function getRawContent($id, $part = null)
{
if ($part !== null) {
// TODO: implement
throw new Exception\RuntimeException('not implemented');
}
$fh = fopen($this->getFileData($id, 'filename'), 'r');
while (! feof($fh)) {
$line = fgets($fh);
if (! trim($line)) {
break;
}
}
$content = stream_get_contents($fh);
fclose($fh);
return $content;
}
/**
* Create instance with parameters
* Supported parameters are:
* - dirname dirname of mbox file
*
* @param array|object $params Array, iterable object, or stdClass object
* with reader specific parameters
* @throws Exception\InvalidArgumentException
*/
public function __construct($params)
{
$params = ParamsNormalizer::normalizeParams($params);
if (! isset($params['dirname'])) {
throw new Exception\InvalidArgumentException('no dirname provided in params');
}
$dirname = (string) $params['dirname'];
if (! is_dir($dirname)) {
throw new Exception\InvalidArgumentException(sprintf('Maildir "%s" is not a directory', $dirname));
}
if (! $this->isMaildir($dirname)) {
throw new Exception\InvalidArgumentException('invalid maildir given');
}
$this->has['top'] = true;
$this->has['flags'] = true;
$this->openMaildir($dirname);
}
/**
* check if a given dir is a valid maildir
*
* @param string $dirname name of dir
* @return bool dir is valid maildir
*/
protected function isMaildir($dirname)
{
if (file_exists($dirname . '/new') && ! is_dir($dirname . '/new')) {
return false;
}
if (file_exists($dirname . '/tmp') && ! is_dir($dirname . '/tmp')) {
return false;
}
return is_dir($dirname . '/cur');
}
/**
* open given dir as current maildir
*
* @param string $dirname name of maildir
* @throws Exception\RuntimeException
*/
protected function openMaildir($dirname)
{
if ($this->files) {
$this->close();
}
ErrorHandler::start(E_WARNING);
$dh = opendir($dirname . '/cur/');
$error = ErrorHandler::stop();
if (! $dh) {
throw new Exception\RuntimeException('cannot open maildir', 0, $error);
}
$this->getMaildirFiles($dh, $dirname . '/cur/');
closedir($dh);
ErrorHandler::start(E_WARNING);
$dh = opendir($dirname . '/new/');
$error = ErrorHandler::stop();
if (! $dh) {
throw new Exception\RuntimeException('cannot read recent mails in maildir', 0, $error);
}
$this->getMaildirFiles($dh, $dirname . '/new/', [Mail\Storage::FLAG_RECENT]);
closedir($dh);
}
/**
* find all files in opened dir handle and add to maildir files
*
* @param resource $dh dir handle used for search
* @param string $dirname dirname of dir in $dh
* @param array $defaultFlags default flags for given dir
*/
protected function getMaildirFiles($dh, $dirname, $defaultFlags = [])
{
while (($entry = readdir($dh)) !== false) {
if ($entry[0] == '.' || ! is_file($dirname . $entry)) {
continue;
}
if (str_contains($entry, ':')) {
[$uniq, $info] = explode(':', $entry, 2);
} else {
$uniq = $entry;
$info = '';
}
if (str_contains($uniq, ',')) {
[, $size] = explode(',', $uniq, 2);
} else {
$size = '';
}
if (strlen($size) >= 2 && $size[0] === 'S' && $size[1] === '=') {
$size = substr($size, 2);
}
if (! ctype_digit($size)) {
$size = null;
}
if (str_contains($info, ',')) {
[$version, $flags] = explode(',', $info, 2);
} else {
$version = $info;
$flags = '';
}
if ($version !== '2') {
$flags = '';
}
$namedFlags = $defaultFlags;
$length = strlen($flags);
for ($i = 0; $i < $length; ++$i) {
$flag = $flags[$i];
$namedFlags[$flag] = static::$knownFlags[$flag] ?? $flag;
}
$data = [
'uniq' => $uniq,
'flags' => $namedFlags,
'flaglookup' => array_flip($namedFlags),
'filename' => $dirname . $entry,
];
if ($size !== null) {
$data['size'] = (int) $size;
}
$this->files[] = $data;
}
usort($this->files, static fn($a, $b): int => strcmp($a['filename'], $b['filename']));
}
/**
* Close resource for mail lib. If you need to control, when the resource
* is closed. Otherwise the destructor would call this.
*/
public function close()
{
$this->files = [];
}
/**
* Waste some CPU cycles doing nothing.
*
* @return bool always return true
*/
public function noop()
{
return true;
}
/**
* stub for not supported message deletion
*
* @param int $id
* @throws Exception\RuntimeException
*/
public function removeMessage($id)
{
throw new Exception\RuntimeException('maildir is (currently) read-only');
}
/**
* get unique id for one or all messages
*
* if storage does not support unique ids it's the same as the message number
*
* @param int|null $id message number
* @return array|string message number for given message or all messages as array
*/
public function getUniqueId($id = null)
{
if ($id) {
return $this->getFileData($id, 'uniq');
}
$ids = [];
foreach ($this->files as $num => $file) {
$ids[$num + 1] = $file['uniq'];
}
return $ids;
}
/**
* get a message number from a unique id
*
* I.e. if you have a webmailer that supports deleting messages you should use unique ids
* as parameter and use this method to translate it to message number right before calling removeMessage()
*
* @param string $id unique id
* @throws Exception\InvalidArgumentException
* @return int message number
*/
public function getNumberByUniqueId($id)
{
foreach ($this->files as $num => $file) {
if ($file['uniq'] == $id) {
return $num + 1;
}
}
throw new Exception\InvalidArgumentException('unique id not found');
}
}

View File

@@ -1,440 +0,0 @@
<?php
namespace Laminas\Mail\Storage;
use Laminas\Mail\Storage\Exception\ExceptionInterface;
use Laminas\Mail\Storage\Message\File;
use Laminas\Mail\Storage\Message\MessageInterface;
use Laminas\Stdlib\ErrorHandler;
use function array_combine;
use function count;
use function fclose;
use function fgets;
use function filemtime;
use function fopen;
use function fseek;
use function ftell;
use function is_dir;
use function is_resource;
use function is_subclass_of;
use function range;
use function str_starts_with;
use function stream_get_contents;
use function strlen;
use function strtolower;
use function trim;
use const E_WARNING;
class Mbox extends AbstractStorage
{
/**
* file handle to mbox file
*
* @var null|resource
*/
protected $fh;
/**
* filename of mbox file for __wakeup
*
* @var string
*/
protected $filename;
/**
* modification date of mbox file for __wakeup
*
* @var int
*/
protected $filemtime;
/**
* start and end position of messages as array('start' => start, 'separator' => headersep, 'end' => end)
*
* @var array
*/
protected $positions;
/**
* used message class, change it in an extended class to extend the returned message class
*
* @var class-string<MessageInterface>
*/
protected $messageClass = File::class;
/**
* end of Line for messages
*
* @var string|null
*/
// phpcs:ignore WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCapsProperty
protected $messageEOL;
/**
* Count messages all messages in current box
*
* @return int number of messages
* @throws ExceptionInterface
*/
public function countMessages()
{
return count($this->positions);
}
/**
* Get a list of messages with number and size
*
* @param int|null $id number of message or null for all messages
* @return int|array size of given message of list with all messages as array(num => size)
*/
public function getSize($id = 0)
{
if ($id) {
$pos = $this->positions[$id - 1];
return $pos['end'] - $pos['start'];
}
$result = [];
foreach ($this->positions as $num => $pos) {
$result[$num + 1] = $pos['end'] - $pos['start'];
}
return $result;
}
/**
* Get positions for mail message or throw exception if id is invalid
*
* @param int $id number of message
* @throws Exception\InvalidArgumentException
* @return array positions as in positions
*/
protected function getPos($id)
{
if (! isset($this->positions[$id - 1])) {
throw new Exception\InvalidArgumentException('id does not exist');
}
return $this->positions[$id - 1];
}
/**
* Fetch a message
*
* @param int $id number of message
* @return File
* @throws ExceptionInterface
*/
public function getMessage($id)
{
// TODO that's ugly, would be better to let the message class decide
if (
is_subclass_of($this->messageClass, File::class)
|| strtolower($this->messageClass) === strtolower(File::class)
) {
// TODO top/body lines
$messagePos = $this->getPos($id);
$messageClassParams = [
'file' => $this->fh,
'startPos' => $messagePos['start'],
'endPos' => $messagePos['end'],
];
if (isset($this->messageEOL)) {
$messageClassParams['EOL'] = $this->messageEOL;
}
return new $this->messageClass($messageClassParams);
}
/** @todo Uncomment once we know how to count body lines */
// $bodyLines = 0;
$message = $this->getRawHeader($id);
/* Once we know how to count body lines, we should uncomment the
* following, which would append the body content to the headers.
*
if ($bodyLines) {
$message .= "\n";
while ($bodyLines-- && ftell($this->fh) < $this->positions[$id - 1]['end']) {
$message .= fgets($this->fh);
}
}
*/
return new $this->messageClass(['handler' => $this, 'id' => $id, 'headers' => $message]);
}
/**
* Get raw header of message or part
*
* @param int $id number of message
* @param null|array|string $part path to part or null for message header
* @param int $topLines include this many lines with header (after an empty line)
* @return string raw header
* @throws \Laminas\Mail\Protocol\Exception\ExceptionInterface
* @throws ExceptionInterface
*/
public function getRawHeader($id, $part = null, $topLines = 0)
{
if ($part !== null) {
// TODO: implement
throw new Exception\RuntimeException('not implemented');
}
$messagePos = $this->getPos($id);
// TODO: toplines
return stream_get_contents($this->fh, $messagePos['separator'] - $messagePos['start'], $messagePos['start']);
}
/**
* Get raw content of message or part
*
* @param int $id number of message
* @param null|array|string $part path to part or null for message content
* @return string raw content
* @throws \Laminas\Mail\Protocol\Exception\ExceptionInterface
* @throws ExceptionInterface
*/
public function getRawContent($id, $part = null)
{
if ($part !== null) {
// TODO: implement
throw new Exception\RuntimeException('not implemented');
}
$messagePos = $this->getPos($id);
return stream_get_contents($this->fh, $messagePos['end'] - $messagePos['separator'], $messagePos['separator']);
}
/**
* Create instance with parameters
* Supported parameters are:
* - filename filename of mbox file
*
* @param array|object|Config $params mail reader specific parameters
* @throws Exception\InvalidArgumentException
*/
public function __construct($params)
{
$params = ParamsNormalizer::normalizeParams($params);
if (! isset($params['filename'])) {
throw new Exception\InvalidArgumentException('no valid filename given in params');
}
if (isset($params['messageEOL'])) {
$this->messageEOL = (string) $params['messageEOL'];
}
$this->openMboxFile((string) $params['filename']);
$this->has['top'] = true;
$this->has['uniqueid'] = false;
}
/**
* check if given file is a mbox file
*
* if $file is a resource its file pointer is moved after the first line
*
* @param resource|string $file stream resource of name of file
* @param bool $fileIsString file is string or resource
* @return bool file is mbox file
*/
protected function isMboxFile($file, $fileIsString = true)
{
if ($fileIsString) {
ErrorHandler::start(E_WARNING);
$file = fopen($file, 'r');
ErrorHandler::stop();
if (! $file) {
return false;
}
} else {
fseek($file, 0);
}
$result = false;
$line = fgets($file) ?: '';
if (str_starts_with($line, 'From ')) {
$result = true;
}
if ($fileIsString) {
ErrorHandler::start(E_WARNING);
fclose($file);
ErrorHandler::stop();
}
return $result;
}
/**
* open given file as current mbox file
*
* @param string $filename filename of mbox file
* @throws Exception\RuntimeException
* @throws Exception\InvalidArgumentException
*/
protected function openMboxFile($filename)
{
if ($this->fh) {
$this->close();
}
if (is_dir($filename)) {
throw new Exception\InvalidArgumentException('file is not a valid mbox file');
}
ErrorHandler::start();
$this->fh = fopen($filename, 'r');
$error = ErrorHandler::stop();
if (! $this->fh) {
throw new Exception\RuntimeException('cannot open mbox file', 0, $error);
}
$this->filename = $filename;
$this->filemtime = filemtime($this->filename);
if (! $this->isMboxFile($this->fh, false)) {
ErrorHandler::start(E_WARNING);
fclose($this->fh);
$error = ErrorHandler::stop();
throw new Exception\InvalidArgumentException('file is not a valid mbox format', 0, $error);
}
$messagePos = ['start' => ftell($this->fh), 'separator' => 0, 'end' => 0];
while (($line = fgets($this->fh)) !== false) {
if (str_starts_with($line, 'From ')) {
$messagePos['end'] = ftell($this->fh) - strlen($line) - 2; // + newline
if (! $messagePos['separator']) {
$messagePos['separator'] = $messagePos['end'];
}
$this->positions[] = $messagePos;
$messagePos = ['start' => ftell($this->fh), 'separator' => 0, 'end' => 0];
}
if (! $messagePos['separator'] && ! trim($line)) {
$messagePos['separator'] = ftell($this->fh);
}
}
$messagePos['end'] = ftell($this->fh);
if (! $messagePos['separator']) {
$messagePos['separator'] = $messagePos['end'];
}
$this->positions[] = $messagePos;
}
/**
* Close resource for mail lib. If you need to control, when the resource
* is closed. Otherwise the destructor would call this.
*/
public function close()
{
if (is_resource($this->fh)) {
fclose($this->fh);
}
$this->positions = [];
}
/**
* Waste some CPU cycles doing nothing.
*
* @return bool always return true
*/
public function noop()
{
return true;
}
/**
* stub for not supported message deletion
*
* @param int $id message number
* @throws Exception\RuntimeException
*/
public function removeMessage($id)
{
throw new Exception\RuntimeException('mbox is read-only');
}
/**
* get unique id for one or all messages
*
* Mbox does not support unique ids (yet) - it's always the same as the message number.
* That shouldn't be a problem, because we can't change mbox files. Therefor the message
* number is save enough.
*
* @param int|null $id message number
* @return array|string message number for given message or all messages as array
* @throws ExceptionInterface
*/
public function getUniqueId($id = null)
{
if ($id) {
// check if id exists
$this->getPos($id);
return $id;
}
$range = range(1, $this->countMessages());
return array_combine($range, $range);
}
/**
* get a message number from a unique id
*
* I.e. if you have a webmailer that supports deleting messages you should use unique ids
* as parameter and use this method to translate it to message number right before calling removeMessage()
*
* @param string $id unique id
* @return int message number
* @throws ExceptionInterface
*/
public function getNumberByUniqueId($id)
{
// check if id exists
$this->getPos($id);
return $id;
}
/**
* magic method for serialize()
*
* with this method you can cache the mbox class
*
* @return array name of variables
*/
public function __sleep()
{
return ['filename', 'positions', 'filemtime'];
}
/**
* magic method for unserialize()
*
* with this method you can cache the mbox class
* for cache validation the mtime of the mbox file is used
*
* @throws Exception\RuntimeException
*/
public function __wakeup()
{
ErrorHandler::start();
$filemtime = filemtime($this->filename);
ErrorHandler::stop();
if ($this->filemtime != $filemtime) {
$this->close();
$this->openMboxFile($this->filename);
} else {
ErrorHandler::start();
$this->fh = fopen($this->filename, 'r');
$error = ErrorHandler::stop();
if (! $this->fh) {
throw new Exception\RuntimeException('cannot open mbox file', 0, $error);
}
}
}
}

View File

@@ -1,87 +0,0 @@
<?php
namespace Laminas\Mail\Storage;
use Laminas\Stdlib\ErrorHandler;
use function array_combine;
use function file_get_contents;
use function is_resource;
use function ltrim;
use function stream_get_contents;
class Message extends Part implements Message\MessageInterface
{
/**
* flags for this message
*
* @var array
*/
protected $flags = [];
/**
* Public constructor
*
* In addition to the parameters of Part::__construct() this constructor supports:
* - file filename or file handle of a file with raw message content
* - flags array with flags for message, keys are ignored, use constants defined in \Laminas\Mail\Storage
*
* @param array $params
* @throws Exception\RuntimeException
*/
public function __construct(array $params)
{
if (isset($params['file'])) {
if (! is_resource($params['file'])) {
ErrorHandler::start();
$params['raw'] = file_get_contents($params['file']);
$error = ErrorHandler::stop();
if ($params['raw'] === false) {
throw new Exception\RuntimeException('could not open file', 0, $error);
}
} else {
$params['raw'] = stream_get_contents($params['file']);
}
$params['raw'] = ltrim($params['raw']);
}
if (! empty($params['flags'])) {
// set key and value to the same value for easy lookup
$this->flags = array_combine($params['flags'], $params['flags']);
}
parent::__construct($params);
}
/**
* return toplines as found after headers
*
* @return string toplines
*/
public function getTopLines()
{
return $this->topLines;
}
/**
* check if flag is set
*
* @param mixed $flag a flag name, use constants defined in \Laminas\Mail\Storage
* @return bool true if set, otherwise false
*/
public function hasFlag($flag)
{
return isset($this->flags[$flag]);
}
/**
* get all set flags
*
* @return array array with flags, key and value are the same for easy lookup
*/
public function getFlags()
{
return $this->flags;
}
}

View File

@@ -1,68 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Message;
use Laminas\Mail\Storage\Exception\ExceptionInterface;
use Laminas\Mail\Storage\Part;
use function array_combine;
class File extends Part\File implements MessageInterface
{
/**
* flags for this message
*
* @var array
*/
protected $flags = [];
/**
* Public constructor
*
* In addition to the parameters of Laminas\Mail\Storage\Part::__construct() this constructor supports:
* - flags array with flags for message, keys are ignored, use constants defined in Laminas\Mail\Storage
*
* @param array $params
* @throws ExceptionInterface
*/
public function __construct(array $params)
{
if (! empty($params['flags'])) {
// set key and value to the same value for easy lookup
$this->flags = array_combine($params['flags'], $params['flags']);
}
parent::__construct($params);
}
/**
* return toplines as found after headers
*
* @return string toplines
*/
public function getTopLines()
{
return $this->topLines;
}
/**
* check if flag is set
*
* @param mixed $flag a flag name, use constants defined in \Laminas\Mail\Storage
* @return bool true if set, otherwise false
*/
public function hasFlag($flag)
{
return isset($this->flags[$flag]);
}
/**
* get all set flags
*
* @return array array with flags, key and value are the same for easy lookup
*/
public function getFlags()
{
return $this->flags;
}
}

View File

@@ -1,28 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Message;
interface MessageInterface
{
/**
* return toplines as found after headers
*
* @return string toplines
*/
public function getTopLines();
/**
* check if flag is set
*
* @param mixed $flag a flag name, use constants defined in Laminas\Mail\Storage
* @return bool true if set, otherwise false
*/
public function hasFlag($flag);
/**
* get all set flags
*
* @return array array with flags, key and value are the same for easy lookup
*/
public function getFlags();
}

View File

@@ -1,43 +0,0 @@
<?php
namespace Laminas\Mail\Storage;
use Traversable;
use Webmozart\Assert\Assert;
use function get_object_vars;
use function gettype;
use function is_array;
use function is_object;
use function iterator_to_array;
use function sprintf;
/**
* @internal
*/
final class ParamsNormalizer
{
/**
* @return array<string, mixed>
*/
public static function normalizeParams(mixed $params): array
{
if ($params instanceof Traversable) {
$params = iterator_to_array($params);
}
if (is_object($params)) {
$params = get_object_vars($params);
}
if (! is_array($params)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid $params provided; expected array|Traversable|object, received %s',
gettype($params)
));
}
Assert::isMap($params, 'Expected $params to have only string keys');
return $params;
}
}

View File

@@ -1,496 +0,0 @@
<?php
namespace Laminas\Mail\Storage;
use ArrayIterator;
use Laminas\Mail\Header\HeaderInterface;
use Laminas\Mail\Headers;
use Laminas\Mime;
use Laminas\Mime\Exception\RuntimeException;
use RecursiveIterator;
use ReturnTypeWillChange;
use Stringable;
use function array_map;
use function count;
use function current;
use function implode;
use function is_array;
use function iterator_to_array;
use function preg_replace;
use function stripos;
use function strlen;
use function strtolower;
use function trim;
class Part implements RecursiveIterator, Part\PartInterface, Stringable
{
/**
* Headers of the part
*
* @var Headers|null
*/
protected $headers;
/**
* raw part body
*
* @var null|string
*/
protected $content;
/**
* toplines as fetched with headers
*
* @var string
*/
protected $topLines = '';
/**
* parts of multipart message
*
* @var array
*/
protected $parts = [];
/**
* count of parts of a multipart message
*
* @var null|int
*/
protected $countParts;
/**
* current position of iterator
*
* @var int
*/
protected $iterationPos = 1;
/**
* mail handler, if late fetch is active
*
* @var null|AbstractStorage
*/
protected $mail;
/**
* message number for mail handler
*
* @var int
*/
protected $messageNum = 0;
/**
* Public constructor
*
* Part supports different sources for content. The possible params are:
* - handler an instance of AbstractStorage for late fetch
* - id number of message for handler
* - raw raw content with header and body as string
* - headers headers as array (name => value) or string, if a content part is found it's used as toplines
* - noToplines ignore content found after headers in param 'headers'
* - content content as string
* - strict strictly parse raw content
*
* @param array $params full message with or without headers
* @throws Exception\InvalidArgumentException
*/
public function __construct(array $params)
{
if (isset($params['handler'])) {
if (! $params['handler'] instanceof AbstractStorage) {
throw new Exception\InvalidArgumentException('handler is not a valid mail handler');
}
if (! isset($params['id'])) {
throw new Exception\InvalidArgumentException('need a message id with a handler');
}
$this->mail = $params['handler'];
$this->messageNum = $params['id'];
}
$params['strict'] ??= false;
if (isset($params['raw'])) {
Mime\Decode::splitMessage(
$params['raw'],
$this->headers,
$this->content,
Mime\Mime::LINEEND,
$params['strict']
);
} elseif (isset($params['headers'])) {
if (is_array($params['headers'])) {
$this->headers = new Headers();
$this->headers->addHeaders($params['headers']);
} else {
if (empty($params['noToplines'])) {
Mime\Decode::splitMessage($params['headers'], $this->headers, $this->topLines);
} else {
$this->headers = Headers::fromString($params['headers']);
}
}
if (isset($params['content'])) {
$this->content = $params['content'];
}
}
}
/**
* Check if part is a multipart message
*
* @return bool if part is multipart
*/
public function isMultipart()
{
try {
return stripos($this->contentType, 'multipart/') === 0;
} catch (Exception\ExceptionInterface) {
return false;
}
}
/**
* Body of part
*
* If part is multipart the raw content of this part with all sub parts is returned
*
* @throws Exception\RuntimeException
* @return string body
*/
public function getContent()
{
if ($this->content !== null) {
return $this->content;
}
if ($this->mail) {
return $this->mail->getRawContent($this->messageNum);
}
throw new Exception\RuntimeException('no content');
}
/**
* Return size of part
*
* Quite simple implemented currently (not decoding). Handle with care.
*
* @return int size
*/
public function getSize()
{
return strlen($this->getContent());
}
/**
* Cache content and split in parts if multipart
*
* @throws Exception\RuntimeException
* @return void
*/
protected function cacheContent()
{
// caching content if we can't fetch parts
if ($this->content === null && $this->mail) {
$this->content = $this->mail->getRawContent($this->messageNum);
}
if (! $this->isMultipart()) {
return;
}
// split content in parts
$boundary = $this->getHeaderField('content-type', 'boundary');
if (! $boundary) {
throw new Exception\RuntimeException('no boundary found in content type to split message');
}
$parts = Mime\Decode::splitMessageStruct($this->content, $boundary);
if ($parts === null) {
return;
}
$counter = 1;
foreach ($parts as $part) {
$this->parts[$counter++] = new static(['headers' => $part['header'], 'content' => $part['body']]);
}
}
/**
* Get part of multipart message
*
* @param int $num number of part starting with 1 for first part
* @throws Exception\RuntimeException
* @return Part wanted part
*/
public function getPart($num)
{
if (isset($this->parts[$num])) {
return $this->parts[$num];
}
if (! $this->mail && $this->content === null) {
throw new Exception\RuntimeException('part not found');
}
// if ($this->mail && $this->mail->hasFetchPart) {
// TODO: fetch part
// return
// }
$this->cacheContent();
if (! isset($this->parts[$num])) {
throw new Exception\RuntimeException('part not found');
}
return $this->parts[$num];
}
/**
* Count parts of a multipart part
*
* @return int number of sub-parts
*/
public function countParts()
{
if ($this->countParts) {
return $this->countParts;
}
$this->countParts = count($this->parts);
if ($this->countParts) {
return $this->countParts;
}
// if ($this->mail && $this->mail->hasFetchPart) {
// TODO: fetch part
// return
// }
$this->cacheContent();
$this->countParts = count($this->parts);
return $this->countParts;
}
/**
* Access headers collection
*
* Lazy-loads if not already attached.
*
* @return Headers
* @throws Exception\RuntimeException
*/
public function getHeaders()
{
if (null === $this->headers) {
if ($this->mail) {
$part = $this->mail->getRawHeader($this->messageNum);
$this->headers = Headers::fromString($part);
} else {
$this->headers = new Headers();
}
}
if (! $this->headers instanceof Headers) {
throw new Exception\RuntimeException(
'$this->headers must be an instance of Headers'
);
}
return $this->headers;
}
/**
* Get a header in specified format
*
* Internally headers that occur more than once are saved as array, all other as string. If $format
* is set to string implode is used to concat the values (with Mime::LINEEND as delim).
*
* @param string $name name of header, matches case-insensitive, but camel-case is replaced with dashes
* @param string $format change type of return value to 'string' or 'array'
* @throws Exception\InvalidArgumentException
* @return string|array|HeaderInterface|ArrayIterator value of header in wanted or internal format
*/
public function getHeader($name, $format = null)
{
$header = $this->getHeaders()->get($name);
if ($header === false) {
$lowerName = strtolower(preg_replace('%([a-z])([A-Z])%', '\1-\2', $name));
$header = $this->getHeaders()->get($lowerName);
if ($header === false) {
throw new Exception\InvalidArgumentException(
"Header with Name $name or $lowerName not found"
);
}
}
switch ($format) {
case 'string':
if ($header instanceof HeaderInterface) {
$return = $header->getFieldValue(HeaderInterface::FORMAT_RAW);
} else {
$return = trim(implode(
Mime\Mime::LINEEND,
array_map(static fn($header): string
=> $header->getFieldValue(HeaderInterface::FORMAT_RAW), iterator_to_array($header))
), Mime\Mime::LINEEND);
}
break;
case 'array':
if ($header instanceof HeaderInterface) {
$return = [$header->getFieldValue()];
} else {
$return = [];
foreach ($header as $h) {
$return[] = $h->getFieldValue(HeaderInterface::FORMAT_RAW);
}
}
break;
default:
$return = $header;
}
return $return;
}
/**
* Get a specific field from a header like content type or all fields as array
*
* If the header occurs more than once, only the value from the first header
* is returned.
*
* Throws an Exception if the requested header does not exist. If
* the specific header field does not exist, returns null.
*
* @param string $name name of header, like in getHeader()
* @param string $wantedPart the wanted part, default is first, if null an array with all parts is returned
* @param string $firstName key name for the first part
* @return string|array wanted part or all parts as array($firstName => firstPart, partname => value)
* @throws RuntimeException
*/
public function getHeaderField($name, $wantedPart = '0', $firstName = '0')
{
return Mime\Decode::splitHeaderField(current($this->getHeader($name, 'array')), $wantedPart, $firstName);
}
/**
* Getter for mail headers - name is matched in lowercase
*
* This getter is short for Part::getHeader($name, 'string')
*
* @see Part::getHeader()
*
* @param string $name header name
* @return string value of header
* @throws Exception\ExceptionInterface
*/
public function __get($name)
{
return $this->getHeader($name, 'string');
}
/**
* Isset magic method proxy to hasHeader
*
* This method is short syntax for Part::hasHeader($name);
*
* @see Part::hasHeader
*
* @param string $name
* @return bool
*/
public function __isset($name)
{
return $this->getHeaders()->has($name);
}
/**
* magic method to get content of part
*
* @return string content
*/
public function __toString(): string
{
return $this->getContent();
}
/**
* implements RecursiveIterator::hasChildren()
*
* @return bool current element has children/is multipart
*/
#[ReturnTypeWillChange]
public function hasChildren()
{
$current = $this->current();
return $current && $current instanceof self && $current->isMultipart();
}
/**
* implements RecursiveIterator::getChildren()
*
* @return Part same as self::current()
*/
#[ReturnTypeWillChange]
public function getChildren()
{
return $this->current();
}
/**
* implements Iterator::valid()
*
* @return bool check if there's a current element
*/
#[ReturnTypeWillChange]
public function valid()
{
if ($this->countParts === null) {
$this->countParts();
}
return $this->iterationPos && $this->iterationPos <= $this->countParts;
}
/**
* implements Iterator::next()
*/
#[ReturnTypeWillChange]
public function next()
{
++$this->iterationPos;
}
/**
* implements Iterator::key()
*
* @return string key/number of current part
*/
#[ReturnTypeWillChange]
public function key()
{
return $this->iterationPos;
}
/**
* implements Iterator::current()
*
* @return Part current part
*/
#[ReturnTypeWillChange]
public function current()
{
return $this->getPart($this->iterationPos);
}
/**
* implements Iterator::rewind()
*/
#[ReturnTypeWillChange]
public function rewind()
{
$this->countParts();
$this->iterationPos = 1;
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Part\Exception;
use Laminas\Mail\Storage\Exception\ExceptionInterface as StorageException;
interface ExceptionInterface extends StorageException
{
}

View File

@@ -1,12 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Part\Exception;
use Laminas\Mail\Storage\Exception;
/**
* Exception for Laminas\Mail component.
*/
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -1,12 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Part\Exception;
use Laminas\Mail\Storage\Exception;
/**
* Exception for Laminas\Mail component.
*/
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}

View File

@@ -1,171 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Part;
use Laminas\Mail\Headers;
use Laminas\Mail\Storage\Part;
use function count;
use function feof;
use function fgets;
use function fopen;
use function fread;
use function fseek;
use function ftell;
use function is_resource;
use function stream_copy_to_stream;
use function trim;
use const SEEK_END;
class File extends Part
{
/** @var array */
protected $contentPos = [];
/** @var array */
protected $partPos = [];
/** @var resource */
protected $fh;
/**
* Public constructor
*
* This handler supports the following params:
* - file filename or open file handler with message content (required)
* - startPos start position of message or part in file (default: current position)
* - endPos end position of message or part in file (default: end of file)
* - EOL end of Line for messages
*
* @param array $params full message with or without headers
* @throws Exception\RuntimeException
* @throws Exception\InvalidArgumentException
*/
public function __construct(array $params)
{
if (empty($params['file'])) {
throw new Exception\InvalidArgumentException('no file given in params');
}
if (! is_resource($params['file'])) {
$fh = fopen($params['file'], 'r');
} else {
$fh = $params['file'];
}
if (! $fh) {
throw new Exception\RuntimeException('could not open file');
}
$this->fh = $fh;
if (isset($params['startPos'])) {
fseek($this->fh, $params['startPos']);
}
$header = '';
$endPos = $params['endPos'] ?? null;
while (($endPos === null || ftell($this->fh) < $endPos) && trim($line = fgets($this->fh))) {
$header .= $line;
}
if (isset($params['EOL'])) {
$this->headers = Headers::fromString($header, $params['EOL']);
} else {
$this->headers = Headers::fromString($header);
}
$this->contentPos[0] = ftell($this->fh);
if ($endPos !== null) {
$this->contentPos[1] = $endPos;
} else {
fseek($this->fh, 0, SEEK_END);
$this->contentPos[1] = ftell($this->fh);
}
if (! $this->isMultipart()) {
return;
}
$boundary = $this->getHeaderField('content-type', 'boundary');
if (! $boundary) {
throw new Exception\RuntimeException('no boundary found in content type to split message');
}
$part = [];
$pos = $this->contentPos[0];
fseek($this->fh, $pos);
while (! feof($this->fh) && ($endPos === null || $pos < $endPos)) {
$line = fgets($this->fh);
if ($line === false) {
if (feof($this->fh)) {
break;
}
throw new Exception\RuntimeException('error reading file');
}
$lastPos = $pos;
$pos = ftell($this->fh);
$line = trim($line);
if ($line == '--' . $boundary) {
if ($part) {
// not first part
$part[1] = $lastPos;
$this->partPos[] = $part;
}
$part = [$pos];
} elseif ($line == '--' . $boundary . '--') {
$part[1] = $lastPos;
$this->partPos[] = $part;
break;
}
}
$this->countParts = count($this->partPos);
}
/**
* Body of part
*
* If part is multipart the raw content of this part with all sub parts is returned
*
* @param resource $stream Optional
* @return string body
*/
public function getContent($stream = null)
{
fseek($this->fh, $this->contentPos[0]);
if ($stream !== null) {
return stream_copy_to_stream($this->fh, $stream, $this->contentPos[1] - $this->contentPos[0]);
}
$length = $this->contentPos[1] - $this->contentPos[0];
return $length < 1 ? '' : fread($this->fh, $length);
}
/**
* Return size of part
*
* Quite simple implemented currently (not decoding). Handle with care.
*
* @return int size
*/
public function getSize()
{
return $this->contentPos[1] - $this->contentPos[0];
}
/**
* Get part of multipart message
*
* @param int $num number of part starting with 1 for first part
* @throws Exception\RuntimeException
* @return Part wanted part
*/
public function getPart($num)
{
--$num;
if (! isset($this->partPos[$num])) {
throw new Exception\RuntimeException('part not found');
}
return new static([
'file' => $this->fh,
'startPos' => $this->partPos[$num][0],
'endPos' => $this->partPos[$num][1],
]);
}
}

View File

@@ -1,118 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Part;
use ArrayIterator;
use Laminas\Mail\Header\HeaderInterface;
use Laminas\Mail\Headers;
use RecursiveIterator;
interface PartInterface extends RecursiveIterator
{
/**
* Check if part is a multipart message
*
* @return bool if part is multipart
*/
public function isMultipart();
/**
* Body of part
*
* If part is multipart the raw content of this part with all sub parts is
* returned.
*
* @return string body
* @throws Exception\ExceptionInterface
*/
public function getContent();
/**
* Return size of part
*
* @return int size
*/
public function getSize();
/**
* Get part of multipart message
*
* @param int $num number of part starting with 1 for first part
* @return PartInterface wanted part
* @throws Exception\ExceptionInterface
*/
public function getPart($num);
/**
* Count parts of a multipart part
*
* @return int number of sub-parts
*/
public function countParts();
/**
* Get all headers
*
* The returned headers are as saved internally. All names are lowercased.
* The value is a string or an array if a header with the same name occurs
* more than once.
*
* @return Headers
*/
public function getHeaders();
/**
* Get a header in specified format
*
* Internally headers that occur more than once are saved as array, all
* other as string. If $format is set to string implode is used to concat
* the values (with Laminas\Mime\Mime::LINEEND as delim).
*
* @param string $name name of header, matches case-insensitive, but
* camel-case is replaced with dashes
* @param string $format change type of return value to 'string' or 'array'
* @return string|array|HeaderInterface|ArrayIterator value of header in
* wanted or internal format
* @throws Exception\ExceptionInterface
*/
public function getHeader($name, $format = null);
/**
* Get a specific field from a header like content type or all fields as array
*
* If the header occurs more than once, only the value from the first
* header is returned.
*
* Throws an exception if the requested header does not exist. If the
* specific header field does not exist, returns null.
*
* @param string $name name of header, like in getHeader()
* @param string $wantedPart the wanted part, default is first, if null an
* array with all parts is returned
* @param string $firstName key name for the first part
* @return string|array wanted part or all parts as
* [$firstName => firstPart, partname => value]
* @throws Exception\ExceptionInterface
*/
public function getHeaderField($name, $wantedPart = '0', $firstName = '0');
/**
* Getter for mail headers - name is matched in lowercase
*
* This getter is short for PartInterface::getHeader($name, 'string')
*
* @see PartInterface::getHeader()
*
* @param string $name header name
* @return string value of header
* @throws Exception\ExceptionInterface
*/
public function __get($name);
/**
* magic method to get content of part
*
* @return string content
*/
public function __toString();
}

View File

@@ -1,300 +0,0 @@
<?php
namespace Laminas\Mail\Storage;
use Laminas\Mail\Exception as MailException;
use Laminas\Mail\Protocol;
use Laminas\Mail\Protocol\Exception\RuntimeException;
use Laminas\Mail\Storage\Exception\ExceptionInterface;
use Laminas\Mail\Storage\Exception\InvalidArgumentException;
use Laminas\Mail\Storage\Message;
use Laminas\Mime;
use function array_combine;
use function array_key_exists;
use function is_string;
use function range;
use function strtolower;
class Pop3 extends AbstractStorage
{
/**
* protocol handler
*
* @var null|\Laminas\Mail\Protocol\Pop3
*/
protected $protocol;
/**
* Count messages all messages in current box
*
* @return int number of messages
* @throws ExceptionInterface
* @throws \Laminas\Mail\Protocol\Exception\ExceptionInterface
*/
public function countMessages()
{
$count = 0; // "Declare" variable before first usage.
$octets = 0; // "Declare" variable since it's passed by reference
$this->protocol->status($count, $octets);
return (int) $count;
}
/**
* get a list of messages with number and size
*
* @param int $id number of message
* @return int|array size of given message of list with all messages as array(num => size)
* @throws \Laminas\Mail\Protocol\Exception\ExceptionInterface
*/
public function getSize($id = 0)
{
$id = $id ?: null;
return $this->protocol->getList($id);
}
/**
* Fetch a message
*
* @param int $id number of message
* @return Message
* @throws \Laminas\Mail\Protocol\Exception\ExceptionInterface
*/
public function getMessage($id)
{
$bodyLines = 0;
$message = $this->protocol->top($id, $bodyLines, true);
return new $this->messageClass([
'handler' => $this,
'id' => $id,
'headers' => $message,
'noToplines' => $bodyLines < 1,
]);
}
/**
* Get raw header of message or part
*
* @param int $id number of message
* @param null|array|string $part path to part or null for message header
* @param int $topLines include this many lines with header (after an empty line)
* @return string raw header
* @throws \Laminas\Mail\Protocol\Exception\ExceptionInterface
* @throws ExceptionInterface
*/
public function getRawHeader($id, $part = null, $topLines = 0)
{
if ($part !== null) {
// TODO: implement
throw new Exception\RuntimeException('not implemented');
}
return $this->protocol->top($id, 0, true);
}
/**
* Get raw content of message or part
*
* @param int $id number of message
* @param null|array|string $part path to part or null for message content
* @return string raw content
* @throws \Laminas\Mail\Protocol\Exception\ExceptionInterface
* @throws ExceptionInterface
*/
public function getRawContent($id, $part = null)
{
if ($part !== null) {
// TODO: implement
throw new Exception\RuntimeException('not implemented');
}
$content = $this->protocol->retrieve($id);
// TODO: find a way to avoid decoding the headers
$headers = null; // "Declare" variable since it's passed by reference
$body = null; // "Declare" variable before first usage.
Mime\Decode::splitMessage($content, $headers, $body);
return $body;
}
/**
* create instance with parameters
* Supported parameters are
* - host hostname or ip address of POP3 server
* - user username
* - password password for user 'username' [optional, default = '']
* - port port for POP3 server [optional, default = 110]
* - ssl 'SSL' or 'TLS' for secure sockets
*
* @param array|object|Protocol\Pop3 $params mail reader specific
* parameters or configured Pop3 protocol object
* @throws InvalidArgumentException
* @throws RuntimeException
*/
public function __construct($params)
{
$this->has['fetchPart'] = false;
$this->has['top'] = null;
$this->has['uniqueid'] = null;
if ($params instanceof Protocol\Pop3) {
$this->protocol = $params;
return;
}
$params = ParamsNormalizer::normalizeParams($params);
if (! isset($params['user'])) {
throw new InvalidArgumentException('need at least user in params');
}
$host = $params['host'] ?? 'localhost';
$password = $params['password'] ?? '';
$port = $params['port'] ?? null;
$ssl = $params['ssl'] ?? false;
if (null !== $port) {
$port = (int) $port;
}
if (! is_string($ssl)) {
$ssl = (bool) $ssl;
}
$this->protocol = new Protocol\Pop3();
if (array_key_exists('novalidatecert', $params)) {
$this->protocol->setNoValidateCert((bool) $params['novalidatecert']);
}
$this->protocol->connect((string) $host, $port, $ssl);
$this->protocol->login((string) $params['user'], (string) $password);
}
/**
* Close resource for mail lib. If you need to control, when the resource
* is closed. Otherwise the destructor would call this.
*/
public function close()
{
$this->protocol->logout();
}
/**
* Keep the server busy.
*
* @throws RuntimeException
*/
public function noop()
{
$this->protocol->noop();
}
/**
* Remove a message from server. If you're doing that from a web environment
* you should be careful and use a uniqueid as parameter if possible to
* identify the message.
*
* @param int $id number of message
* @throws RuntimeException
*/
public function removeMessage($id)
{
$this->protocol->delete($id);
}
/**
* get unique id for one or all messages
*
* if storage does not support unique ids it's the same as the message number
*
* @param int|null $id message number
* @return array|string message number for given message or all messages as array
* @throws ExceptionInterface
*/
public function getUniqueId($id = null)
{
if (! $this->hasUniqueid) {
if ($id) {
return $id;
}
$count = $this->countMessages();
if ($count < 1) {
return [];
}
$range = range(1, $count);
return array_combine($range, $range);
}
return $this->protocol->uniqueid($id);
}
/**
* get a message number from a unique id
*
* I.e. if you have a webmailer that supports deleting messages you should use unique ids
* as parameter and use this method to translate it to message number right before calling removeMessage()
*
* @param string $id unique id
* @throws InvalidArgumentException
* @return int message number
*/
public function getNumberByUniqueId($id)
{
if (! $this->hasUniqueid) {
return $id;
}
$ids = $this->getUniqueId();
foreach ($ids as $k => $v) {
if ($v == $id) {
return $k;
}
}
throw new InvalidArgumentException('unique id not found');
}
/**
* Special handling for hasTop and hasUniqueid. The headers of the first message is
* retrieved if Top wasn't needed/tried yet.
*
* @see AbstractStorage::__get()
*
* @param string $var
* @return null|string
*/
public function __get($var)
{
$result = parent::__get($var);
if ($result !== null) {
return $result;
}
if (strtolower($var) == 'hastop') {
if ($this->protocol->hasTop === null) {
// need to make a real call, because not all server are honest in their capas
try {
$this->protocol->top(1, 0, false);
} catch (MailException\ExceptionInterface) {
// ignoring error
}
}
$this->has['top'] = $this->protocol->hasTop;
return $this->protocol->hasTop;
}
if (strtolower($var) == 'hasuniqueid') {
$id = null;
try {
$id = $this->protocol->uniqueid(1);
} catch (MailException\ExceptionInterface) {
// ignoring error
}
$this->has['uniqueid'] = (bool) $id;
return $this->has['uniqueid'];
}
return $result;
}
}

View File

@@ -1,86 +0,0 @@
<?php
namespace Laminas\Mail\Storage\Writable;
use Laminas\Mail\Message;
use Laminas\Mail\Storage;
use Laminas\Mime;
interface WritableInterface
{
/**
* create a new folder
*
* This method also creates parent folders if necessary. Some mail storages
* may restrict, which folder may be used as parent or which chars may be
* used in the folder name
*
* @param string $name global name of folder, local name if $parentFolder
* is set.
* @param string|Storage\Folder $parentFolder parent folder for new folder,
* else root folder is parent.
* @throws Storage\Exception\ExceptionInterface
*/
public function createFolder($name, $parentFolder = null);
/**
* remove a folder
*
* @param string|Storage\Folder $name name or instance of folder.
* @throws Storage\Exception\ExceptionInterface
*/
public function removeFolder($name);
/**
* rename and/or move folder
*
* The new name has the same restrictions as in createFolder()
*
* @param string|Storage\Folder $oldName name or instance of folder.
* @param string $newName new global name of folder.
* @throws Storage\Exception\ExceptionInterface
*/
public function renameFolder($oldName, $newName);
/**
* append a new message to mail storage
*
* @param string|Message|Mime\Message $message message as string or
* instance of message class.
* @param null|string|Storage\Folder $folder folder for new message, else
* current folder is taken.
* @param null|array $flags set flags for new message, else a default set
* is used.
* @throws Storage\Exception\ExceptionInterface
*/
public function appendMessage($message, $folder = null, $flags = null);
/**
* copy an existing message
*
* @param int $id number of message
* @param string|Storage\Folder $folder name or instance of target folder
* @throws Storage\Exception\ExceptionInterface
*/
public function copyMessage($id, $folder);
/**
* move an existing message
*
* @param int $id number of message
* @param string|Storage\Folder $folder name or instance of target folder
* @throws Storage\Exception\ExceptionInterface
*/
public function moveMessage($id, $folder);
/**
* set flags for message
*
* NOTE: this method can't set the recent flag.
*
* @param int $id number of message
* @param array $flags new flags for message
* @throws Storage\Exception\ExceptionInterface
*/
public function setFlags($id, $flags);
}

View File

@@ -1,57 +0,0 @@
<?php
namespace Laminas\Mail\Transport;
use Laminas\Stdlib\AbstractOptions;
/**
* @extends AbstractOptions<string|list<string>>
*/
class Envelope extends AbstractOptions
{
/** @var string|null */
protected $from;
/** @var string|null */
protected $to;
/**
* Get MAIL FROM
*
* @return string
*/
public function getFrom()
{
return $this->from;
}
/**
* Set MAIL FROM
*
* @param string $from
*/
public function setFrom($from)
{
$this->from = (string) $from;
}
/**
* Get RCPT TO
*
* @return string|null
*/
public function getTo()
{
return $this->to;
}
/**
* Set RCPT TO
*
* @param string $to
*/
public function setTo($to)
{
$this->to = $to;
}
}

View File

@@ -1,12 +0,0 @@
<?php
namespace Laminas\Mail\Transport\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail\Transport component.
*/
class DomainException extends Exception\DomainException implements ExceptionInterface
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Mail\Transport\Exception;
use Laminas\Mail\Exception\ExceptionInterface as MailException;
interface ExceptionInterface extends MailException
{
}

Some files were not shown because too many files have changed in this diff Show More