N°4307 Replace SwiftMailer with Laminas-mail

This commit is contained in:
Stephen Abello
2022-04-22 10:58:28 +02:00
parent c47f224566
commit 178ba60973
518 changed files with 51346 additions and 23096 deletions

View File

@@ -0,0 +1,166 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail;
use Laminas\Validator\EmailAddress as EmailAddressValidator;
use Laminas\Validator\Hostname;
class Address implements Address\AddressInterface
{
protected $comment;
protected $email;
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);
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

@@ -0,0 +1,33 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
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

@@ -0,0 +1,237 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail;
use Countable;
use Iterator;
class AddressList implements Countable, Iterator
{
/**
* List of Address objects we're managing
*
* @var array
*/
protected $addresses = [];
/**
* Add an address to the list
*
* @param string|Address\AddressInterface $emailOrAddress
* @param null|string $name
* @throws Exception\InvalidArgumentException
* @return AddressList
*/
public function add($emailOrAddress, $name = null)
{
if (is_string($emailOrAddress)) {
$emailOrAddress = $this->createAddress($emailOrAddress, $name);
}
if (! $emailOrAddress instanceof Address\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) ? get_class($emailOrAddress) : 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 AddressList
*/
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) ? get_class($key) : var_export($key, 1))
));
}
$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 AddressList
*/
public function addFromString($address, $comment = null)
{
$this->add(Address::fromString($address, $comment));
return $this;
}
/**
* Merge another address list into this one
*
* @param AddressList $addressList
* @return AddressList
*/
public function merge(AddressList $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 bool|Address\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
*/
public function count()
{
return count($this->addresses);
}
/**
* Rewind iterator
*
* @return mixed the value of the first addresses element, or false if the addresses is
* empty.
* @see addresses
*/
public function rewind()
{
return reset($this->addresses);
}
/**
* Return current item in iteration
*
* @return Address
*/
public function current()
{
return current($this->addresses);
}
/**
* Return key of current item of iteration
*
* @return string
*/
public function key()
{
return key($this->addresses);
}
/**
* Move to next item
*
* @return mixed the addresses value in the next place that's pointed to by the
* internal array pointer, or false if there are no more elements.
* @see addresses
*/
public function next()
{
return next($this->addresses);
}
/**
* Is the current item of iteration valid?
*
* @return bool
*/
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

@@ -0,0 +1,42 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
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::class => Protocol\SmtpPluginManager::class,
],
'factories' => [
Protocol\SmtpPluginManager::class => Protocol\SmtpPluginManagerFactory::class,
],
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class BadMethodCallException extends \BadMethodCallException implements
ExceptionInterface
{
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class DomainException extends \DomainException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Exception;
interface ExceptionInterface
{
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class InvalidArgumentException extends \InvalidArgumentException implements
ExceptionInterface
{
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,261 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
use Laminas\Mail\Address;
use Laminas\Mail\AddressList;
use Laminas\Mail\Headers;
use TrueBV\Exception\OutOfBoundsException;
use TrueBV\Punycode;
/**
* Base class for headers composing address lists (to, from, cc, bcc, reply-to)
*/
abstract class AbstractAddressList implements HeaderInterface
{
/**
* @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 Punycode|null
*/
private static $punycode;
public static function fromString($headerLine)
{
list($fieldName, $fieldValue) = GenericHeader::splitHeaderLine($headerLine);
if (strtolower($fieldName) !== static::$type) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid header line for "%s" string',
__CLASS__
));
}
// split value on ","
$fieldValue = str_replace(Headers::FOLDING, ' ', $fieldValue);
$fieldValue = preg_replace('/[^:]+:([^;]*);/', '$1,', $fieldValue);
$values = ListParser::parse($fieldValue);
$wasEncoded = false;
$addresses = array_map(
function ($value) use (&$wasEncoded) {
$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;
}
public function getFieldName()
{
return $this->fieldName;
}
/**
* Safely convert UTF-8 encoded domain name to ASCII
* @param string $domainName the UTF-8 encoded email
* @return string
*/
protected function idnToAscii($domainName)
{
if (null === self::$punycode) {
self::$punycode = new Punycode();
}
try {
return self::$punycode->encode($domainName);
} catch (OutOfBoundsException $e) {
return $domainName;
}
}
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) && (false !== strpos($name, ',') || false !== strpos($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);
}
public function setEncoding($encoding)
{
$this->encoding = $encoding;
return $this;
}
public function getEncoding()
{
return $this->encoding;
}
/**
* Set address list for this header
*
* @param AddressList $addressList
*/
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;
}
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

@@ -0,0 +1,22 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
class Bcc extends AbstractAddressList
{
/**
* @var string
*/
protected $fieldName = 'Bcc';
/**
* @var string
*/
protected static $type = 'bcc';
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
class Cc extends AbstractAddressList
{
protected $fieldName = 'Cc';
protected static $type = 'cc';
}

View File

@@ -0,0 +1,313 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
use Laminas\Mail\Headers;
use Laminas\Mime\Mime;
class ContentDisposition implements UnstructuredInterface
{
/**
* 78 chars (RFC 2822) - (semicolon + space (Header::FOLDING))
*
* @var int
*/
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)
{
list($name, $value) = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (strtolower($name) !== '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, '*')) {
list($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, 1);
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; $i < count($values); $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
$maxValueLength = strlen($value);
do {
$maxValueLength = ceil(0.6 * $maxValueLength);
} while ($maxValueLength > self::MAX_PARAMETER_LENGTH);
if ($valueIsEncoded) {
$encodedLength = strlen($value);
$value = HeaderWrap::mimeDecodeValue($value);
$decodedLength = strlen($value);
$maxValueLength -= ($encodedLength - $decodedLength);
}
$valueParts = str_split($value, $maxValueLength);
$i = 0;
foreach ($valueParts as $valuePart) {
$attributePart = $attribute . '*' . $i++;
if ($valueIsEncoded) {
$valuePart = $this->getEncodedValue($valuePart);
}
$result .= sprintf(';%s%s="%s"', 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

@@ -0,0 +1,114 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
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 = [];
public static function fromString($headerLine)
{
list($name, $value) = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (strtolower($name) !== 'content-transfer-encoding') {
throw new Exception\InvalidArgumentException('Invalid header line for Content-Transfer-Encoding string');
}
$header = new static();
$header->setTransferEncoding($value);
return $header;
}
public function getFieldName()
{
return 'Content-Transfer-Encoding';
}
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return $this->transferEncoding;
}
public function setEncoding($encoding)
{
// Header must be always in US-ASCII
return $this;
}
public function getEncoding()
{
return 'ASCII';
}
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

@@ -0,0 +1,202 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
use Laminas\Mail\Headers;
use Laminas\Mime\Mime;
class ContentType implements UnstructuredInterface
{
/**
* @var string
*/
protected $type;
/**
* Header encoding
*
* @var string
*/
protected $encoding = 'ASCII';
/**
* @var array
*/
protected $parameters = [];
public static function fromString($headerLine)
{
list($name, $value) = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (strtolower($name) !== '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;
}
public function getFieldName()
{
return 'Content-Type';
}
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);
}
public function setEncoding($encoding)
{
$this->encoding = $encoding;
return $this;
}
public function getEncoding()
{
return $this->encoding;
}
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;
}
/**
* 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

@@ -0,0 +1,69 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
/**
* @todo Add accessors for setting date from DateTime, Laminas\Date, or a string
*/
class Date implements HeaderInterface
{
/**
* @var string
*/
protected $value;
public static function fromString($headerLine)
{
list($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');
}
$header = new static($value);
return $header;
}
public function __construct($value)
{
if (! HeaderValue::isValid($value)) {
throw new Exception\InvalidArgumentException('Invalid Date header value detected');
}
$this->value = $value;
}
public function getFieldName()
{
return 'Date';
}
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return $this->value;
}
public function setEncoding($encoding)
{
// This header must be always in US-ASCII
return $this;
}
public function getEncoding()
{
return 'ASCII';
}
public function toString()
{
return 'Date: ' . $this->getFieldValue();
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header\Exception;
use Laminas\Mail\Exception;
class BadMethodCallException extends Exception\BadMethodCallException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header\Exception;
use Laminas\Mail\Exception\ExceptionInterface as MailException;
interface ExceptionInterface extends MailException
{
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header\Exception;
use Laminas\Mail\Exception;
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header\Exception;
use Laminas\Mail\Exception;
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
class From extends AbstractAddressList
{
protected $fieldName = 'From';
protected static $type = 'from';
}

View File

@@ -0,0 +1,198 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
use Laminas\Mime\Mime;
class GenericHeader implements HeaderInterface, UnstructuredInterface
{
/**
* @var string
*/
protected $fieldName = null;
/**
* @var string
*/
protected $fieldValue = null;
/**
* Header encoding
*
* @var null|string
*/
protected $encoding;
/**
* @param string $headerLine
* @return GenericHeader
*/
public static function fromString($headerLine)
{
list($name, $value) = self::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
$header = new static($name, $value);
return $header;
}
/**
* 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 Exception\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 Exception\InvalidArgumentException('Header must match with the format "name:value"');
}
if (! HeaderName::isValid($parts[0])) {
throw new Exception\InvalidArgumentException('Invalid header name detected');
}
if (! HeaderValue::isValid($parts[1])) {
throw new Exception\InvalidArgumentException('Invalid header value detected');
}
$parts[1] = ltrim($parts[1]);
return $parts;
}
/**
* Constructor
*
* @param string $fieldName Optional
* @param string $fieldValue Optional
*/
public function __construct($fieldName = null, $fieldValue = null)
{
if ($fieldName) {
$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 Exception\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 Exception\InvalidArgumentException(
'Header name must be composed of printable US-ASCII characters, except colon.'
);
}
$this->fieldName = $fieldName;
return $this;
}
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 Exception\InvalidArgumentException(
'Header value must be composed of printable US-ASCII characters and valid folding sequences.'
);
}
$this->fieldValue = $fieldValue;
$this->encoding = null;
return $this;
}
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
if (HeaderInterface::FORMAT_ENCODED === $format) {
return HeaderWrap::wrap($this->fieldValue, $this);
}
return $this->fieldValue;
}
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;
}
public function getEncoding()
{
if (! $this->encoding) {
$this->encoding = Mime::isPrintable($this->fieldValue) ? 'ASCII' : 'UTF-8';
}
return $this->encoding;
}
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

@@ -0,0 +1,55 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
/**
* Generic class for Headers with multiple occurs in the same message
*/
class GenericMultiHeader extends GenericHeader implements MultipleHeadersInterface
{
public static function fromString($headerLine)
{
list($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

@@ -0,0 +1,75 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
interface HeaderInterface
{
/**
* Format value in Mime-Encoding (Quoted-Printable). Result is valid US-ASCII string
*
* @var bool
*/
const FORMAT_ENCODED = true;
/**
* Return value in internal encoding which is usually UTF-8
*
* @var bool
*/
const FORMAT_RAW = false;
/**
* Factory to generate a header object from a string
*
* @param string $headerLine
* @return static
* @throws Exception\InvalidArgumentException If the header does not match with RFC 2822 definition.
* @see http://tools.ietf.org/html/rfc2822#section-2.2
*/
public static function fromString($headerLine);
/**
* Retrieve header name
*
* @return string
*/
public function getFieldName();
/**
* Retrieve header value
*
* @param bool $format Return the value in Mime::Encoded or in Raw format
* @return string
*/
public function getFieldValue($format = HeaderInterface::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

@@ -0,0 +1,49 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
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

@@ -0,0 +1,75 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
declare(strict_types=1);
namespace Laminas\Mail\Header;
/**
* Plugin Class Loader implementation for HTTP headers
*/
final class HeaderLocator implements HeaderLocatorInterface
{
/**
* @var array Pre-aliased Header plugins
*/
private $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 isset($this->plugins[$name]) ? $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

@@ -0,0 +1,25 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
declare(strict_types=1);
namespace Laminas\Mail\Header;
/**
* Interface detailing how to resolve header names to classes.
*/
interface HeaderLocatorInterface
{
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

@@ -0,0 +1,73 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
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

@@ -0,0 +1,116 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
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

@@ -0,0 +1,161 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
use Laminas\Mail\Headers;
use Laminas\Mime\Mime;
/**
* Utility class used for creating wrapped or MIME-encoded versions of header
* values.
*/
abstract class HeaderWrap
{
/**
* Wrap a long header line
*
* @param string $value
* @param HeaderInterface $header
* @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
* @param HeaderInterface $header
* @return string
*/
protected static function wrapUnstructuredHeader($value, HeaderInterface $header)
{
$encoding = $header->getEncoding();
if ($encoding == 'ASCII') {
return wordwrap($value, 78, Headers::FOLDING);
}
return static::mimeEncodeValue($value, $encoding, 78);
}
/**
* Wrap a structured header line
*
* @param string $value
* @param StructuredInterface $header
* @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
* @return string Returns the mime encode value without the last line ending
*/
public static function mimeEncodeValue($value, $encoding, $lineLength = 998)
{
return Mime::encodeQuotedPrintableHeader($value, $encoding, $lineLength, Headers::EOL);
}
/**
* 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(
imap_mime_header_decode(imap_utf8($value)),
function ($accumulator, $headerPart) {
return $accumulator . $headerPart->text;
},
''
);
}
return $decodedValue;
}
private static function isNotDecoded($originalValue, $value)
{
return 0 === strpos($value, '=?')
&& strlen($value) - 2 === strpos($value, '?=')
&& false !== strpos($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

@@ -0,0 +1,143 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
use Laminas\Mail\Headers;
/**
* @see https://tools.ietf.org/html/rfc5322#section-3.6.4
*/
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)
{
list($name, $value) = GenericHeader::splitHeaderLine($headerLine);
if (strtolower($name) !== static::$type) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid header line for "%s" string',
__CLASS__
));
}
$value = HeaderWrap::mimeDecodeValue($value);
$messageIds = array_map(
[IdentificationField::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;
}
/**
* @param bool $format
* @return string
*/
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return implode(Headers::FOLDING, array_map(function ($id) {
return 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([IdentificationField::class, "trimMessageId"], $ids);
return $this;
}
/**
* Retrieve the message ids
*
* @return string[]
*/
public function getIds()
{
return $this->messageIds;
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
class InReplyTo extends IdentificationField
{
protected $fieldName = 'In-Reply-To';
protected static $type = 'in-reply-to';
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
use function in_array;
/**
* @internal
*/
class ListParser
{
const CHAR_QUOTES = ['\'', '"'];
const CHAR_DELIMS = [',', ';'];
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

@@ -0,0 +1,119 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
class MessageId implements HeaderInterface
{
/**
* @var string
*/
protected $messageId;
public static function fromString($headerLine)
{
list($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;
}
public function getFieldName()
{
return 'Message-ID';
}
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return $this->messageId;
}
public function setEncoding($encoding)
{
// This header must be always in US-ASCII
return $this;
}
public function getEncoding()
{
return 'ASCII';
}
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

@@ -0,0 +1,87 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
class MimeVersion implements HeaderInterface
{
/**
* @var string Version string
*/
protected $version = '1.0';
public static function fromString($headerLine)
{
list($name, $value) = GenericHeader::splitHeaderLine($headerLine);
$value = HeaderWrap::mimeDecodeValue($value);
// check to ensure proper header type for this factory
if (strtolower($name) !== '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;
}
public function getFieldName()
{
return 'MIME-Version';
}
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return $this->version;
}
public function setEncoding($encoding)
{
// This header must be always in US-ASCII
return $this;
}
public function getEncoding()
{
return 'ASCII';
}
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

@@ -0,0 +1,14 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
interface MultipleHeadersInterface extends HeaderInterface
{
public function toStringMultipleHeaders(array $headers);
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
use Laminas\Mail\Headers;
/**
* @todo Allow setting date from DateTime, Laminas\Date, or string
*/
class Received implements HeaderInterface, MultipleHeadersInterface
{
/**
* @var string
*/
protected $value;
public static function fromString($headerLine)
{
list($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');
}
$header = new static($value);
return $header;
}
public function __construct($value = '')
{
if (! HeaderValue::isValid($value)) {
throw new Exception\InvalidArgumentException('Invalid Received value provided');
}
$this->value = $value;
}
public function getFieldName()
{
return 'Received';
}
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
return $this->value;
}
public function setEncoding($encoding)
{
// This header must be always in US-ASCII
return $this;
}
public function getEncoding()
{
return 'ASCII';
}
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 Received) {
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

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
class References extends IdentificationField
{
protected $fieldName = 'References';
protected static $type = 'references';
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
class ReplyTo extends AbstractAddressList
{
protected $fieldName = 'Reply-To';
protected static $type = 'reply-to';
}

View File

@@ -0,0 +1,153 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
use Laminas\Mail;
use Laminas\Mime\Mime;
/**
* 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 \Laminas\Mail\Address\AddressInterface
*/
protected $address;
/**
* Header encoding
*
* @var null|string
*/
protected $encoding;
public static function fromString($headerLine)
{
list($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;
}
public function getFieldName()
{
return 'Sender';
}
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;
}
public function setEncoding($encoding)
{
$this->encoding = $encoding;
return $this;
}
public function getEncoding()
{
if (! $this->encoding) {
$this->encoding = Mime::isPrintable($this->getFieldValue(HeaderInterface::FORMAT_RAW))
? 'ASCII'
: 'UTF-8';
}
return $this->encoding;
}
public function toString()
{
return 'Sender: ' . $this->getFieldValue(HeaderInterface::FORMAT_ENCODED);
}
/**
* Set the address used in this header
*
* @param string|\Laminas\Mail\Address\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) ? get_class($emailOrAddress) : gettype($emailOrAddress))
));
}
$this->address = $emailOrAddress;
return $this;
}
/**
* Retrieve the internal address from this header
*
* @return \Laminas\Mail\Address\AddressInterface|null
*/
public function getAddress()
{
return $this->address;
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
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

@@ -0,0 +1,119 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
use Laminas\Mime\Mime;
/**
* 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;
public static function fromString($headerLine)
{
list($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;
}
public function getFieldName()
{
return 'Subject';
}
public function getFieldValue($format = HeaderInterface::FORMAT_RAW)
{
if (HeaderInterface::FORMAT_ENCODED === $format) {
return HeaderWrap::wrap($this->subject, $this);
}
return $this->subject;
}
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;
}
public function getEncoding()
{
if (! $this->encoding) {
$this->encoding = Mime::isPrintable($this->subject) ? 'ASCII' : 'UTF-8';
}
return $this->encoding;
}
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;
}
public function toString()
{
return 'Subject: ' . $this->getFieldValue(HeaderInterface::FORMAT_ENCODED);
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
class To extends AbstractAddressList
{
protected $fieldName = 'To';
protected static $type = 'to';
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Header;
/**
* Marker interface for unstructured headers.
*/
interface UnstructuredInterface extends HeaderInterface
{
}

View File

@@ -0,0 +1,606 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
declare(strict_types=1);
namespace Laminas\Mail;
use ArrayIterator;
use Countable;
use Iterator;
use Laminas\Loader\PluginClassLoader;
use Laminas\Loader\PluginClassLocator;
use Laminas\Mail\Header\GenericHeader;
use Laminas\Mail\Header\HeaderInterface;
use Traversable;
/**
* Basic mail headers collection functionality
*
* Handles aggregation of headers
*/
class Headers implements Countable, Iterator
{
/** @var string End of Line for fields */
const EOL = "\r\n";
/** @var string Start of Line when folding */
const FOLDING = "\r\n ";
/**
* @var null|Header\HeaderLocatorInterface
*/
private $headerLocator;
/**
* @todo Remove for 3.0.0.
* @var null|PluginClassLocator
*/
protected $pluginClassLoader;
/**
* @var array key names for $headers array
*/
protected $headersKeys = [];
/**
* @var Header\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}
* @throws Exception\RuntimeException
* @return Headers
*/
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];
// Empty line indicates end of headers
// EXCEPT if there are more lines, in which case, there's a possible error condition
if (preg_match('/^\s*$/', $line)) {
$emptyLine += 1;
if ($emptyLine > 2) {
throw new Exception\RuntimeException('Malformed header detected');
}
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__,
__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__,
__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(): Header\HeaderLocatorInterface
{
if (! $this->headerLocator) {
$this->setHeaderLocator(new Header\HeaderLocator());
}
return $this->headerLocator;
}
/**
* @todo Return self when we update to 7.4 or later as minimum PHP version.
* @return $this
*/
public function setHeaderLocator(Header\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) ? get_class($headers) : 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)
? get_class($headerFieldNameOrLine)
: 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(Header\GenericHeader::fromString($headerFieldNameOrLine . ':' . $fieldValue));
}
return $this;
}
/**
* Add a Header\Interface to this container, for raw values see {@link addHeaderLine()} and {@link addHeaders()}
*
* @param Header\HeaderInterface $header
* @return Headers
*/
public function addHeader(Header\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|Header\HeaderInterface 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__,
Header\HeaderInterface::class,
is_object($instanceOrFieldName) ? get_class($instanceOrFieldName) : 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 bool|ArrayIterator|Header\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) 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);
} else {
return $results[0];
}
//fall-trough
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);
}
/**
* Advance the pointer for this object as an iterator
*
*/
public function next()
{
next($this->headers);
}
/**
* Return the current key for this object as an iterator
*
* @return mixed
*/
public function key()
{
return key($this->headers);
}
/**
* Is this iterator still valid?
*
* @return bool
*/
public function valid()
{
return (current($this->headers) !== false);
}
/**
* Reset the internal pointer for this object as an iterator
*
*/
public function rewind()
{
reset($this->headers);
}
/**
* Return the current value for this iterator, lazy loading it if need be
*
* @return Header\HeaderInterface
*/
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
*/
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
* @todo determine how to produce single line headers, if they are supported
*/
public function toArray($format = Header\HeaderInterface::FORMAT_RAW)
{
$headers = [];
/* @var $header Header\HeaderInterface */
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()
{
foreach ($this as $item) {
// $item should now be loaded
}
return true;
}
/**
* Create Header object from header line
*
* @param string $headerLine
* @return Header\HeaderInterface|Header\HeaderInterface[]
*/
public function loadHeader($headerLine)
{
list($name, ) = Header\GenericHeader::splitHeaderLine($headerLine);
/** @var HeaderInterface $class */
$class = $this->resolveHeaderClass($name);
return $class::fromString($headerLine);
}
/**
* @param $index
* @return mixed
*/
protected function lazyLoadHeader($index)
{
$current = $this->headers[$index];
$key = $this->headersKeys[$index];
/** @var GenericHeader $class */
$class = $this->resolveHeaderClass($key);
$encoding = $current->getEncoding();
$headers = $class::fromString($current->toString());
if (is_array($headers)) {
$current = array_shift($headers);
$current->setEncoding($encoding);
$this->headers[$index] = $current;
foreach ($headers as $header) {
$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 string
*/
private function resolveHeaderClass($key)
{
if ($this->pluginClassLoader) {
return $this->pluginClassLoader->load($key) ?: Header\GenericHeader::class;
}
return $this->getHeaderLocator()->get($key, Header\GenericHeader::class);
}
}

View File

@@ -0,0 +1,576 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail;
use Laminas\Mail\Header\ContentType;
use Laminas\Mail\Header\Sender;
use Laminas\Mime;
use Traversable;
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
*
* @param Headers $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', __NAMESPACE__ . '\Header\From');
}
/**
* 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', __NAMESPACE__ . '\Header\To');
}
/**
* 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', __NAMESPACE__ . '\Header\Cc');
}
/**
* 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', __NAMESPACE__ . '\Header\Bcc');
}
/**
* 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', __NAMESPACE__ . '\Header\ReplyTo');
}
/**
* setSender
*
* @param mixed $emailOrAddress
* @param mixed $name
* @return Message
*/
public function setSender($emailOrAddress, $name = null)
{
/** @var Sender $header */
$header = $this->getHeaderByName('sender', __NAMESPACE__ . '\Header\Sender');
$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', __NAMESPACE__ . '\Header\Sender');
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,
get_class($body)
));
}
}
}
$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', __NAMESPACE__ . '\Header\MimeVersion');
// Multipart content headers
if ($this->body->isMultiPart()) {
$mime = $this->body->getMime();
/** @var ContentType $header */
$header = $this->getHeaderByName('content-type', __NAMESPACE__ . '\Header\ContentType');
$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',
get_class($header)
));
}
return $header->getAddressList();
}
/**
* Update an address list
*
* Proxied to this from addFrom, addTo, addCc, addBcc, and addReplyTo.
*
* @param AddressList $addressList
* @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) ? get_class($emailOrAddressOrList) : 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

@@ -0,0 +1,64 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail;
use Traversable;
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) ? get_class($options) : 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

@@ -0,0 +1,25 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
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

@@ -0,0 +1,356 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol;
use Laminas\Validator;
/**
* 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
*/
const EOL = "\r\n";
/**
* Default timeout in seconds for initiating session
*/
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;
/**
* Port number of connection
* @var int
*/
protected $port;
/**
* Instance of Laminas\Validator\ValidatorChain to check hostnames
* @var \Laminas\Validator\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
* @var array
*/
private $log = [];
/**
* Constructor.
*
* @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', $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;
$this->port = $port;
}
/**
* 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 = [];
}
// @codingStandardsIgnoreStart
/**
* Add the transaction log
*
* @param string $value new transaction
*/
protected function _addLog($value)
{
// @codingStandardsIgnoreEnd
if ($this->maximumLog >= 0 && count($this->log) >= $this->maximumLog) {
array_shift($this->log);
}
$this->log[] = $value;
}
// @codingStandardsIgnoreStart
/**
* 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
*/
protected function _connect($remote)
{
// @codingStandardsIgnoreEnd
$errorNum = 0;
$errorStr = '';
// open connection
set_error_handler(
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;
}
// @codingStandardsIgnoreStart
/**
* Disconnect from remote host and free resource
*
*/
protected function _disconnect()
{
// @codingStandardsIgnoreEnd
if (is_resource($this->socket)) {
fclose($this->socket);
}
}
// @codingStandardsIgnoreStart
/**
* 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
*/
protected function _send($request)
{
// @codingStandardsIgnoreEnd
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;
}
// @codingStandardsIgnoreStart
/**
* Get a line from the stream.
*
* @param int $timeout Per-request timeout value if applicable
* @throws Exception\RuntimeException
* @return string
*/
protected function _receive($timeout = null)
{
// @codingStandardsIgnoreEnd
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 (! empty($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;
}
// @codingStandardsIgnoreStart
/**
* 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
*/
protected function _expect($code, $timeout = null)
{
// @codingStandardsIgnoreEnd
$this->response = [];
$errMsg = '';
if (! is_array($code)) {
$code = [$code];
}
do {
$this->response[] = $result = $this->_receive($timeout);
list($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 (strpos($more, '-') === 0);
if ($errMsg !== '') {
throw new Exception\RuntimeException($errMsg);
}
return $msg;
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol\Exception;
use Laminas\Mail\Exception\ExceptionInterface as MailException;
interface ExceptionInterface extends MailException
{
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,824 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol;
class Imap
{
use ProtocolTrait;
/**
* Default timeout in seconds for initiating session
*/
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 \Laminas\Mail\Protocol\Exception\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 strpos($line, $start) === 0;
}
/**
* 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
list($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 ? $lines : true;
} elseif ($tokens[0] == 'NO') {
return false;
}
return;
}
/**
* 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);
$response = $this->readResponse($tag, $dontParse);
return $response;
}
/**
* escape one or more literals i.e. for sendRequest
*
* @param string|array $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 (strpos($string, "\n") !== false) {
return ['{' . strlen($string) . '}', $string];
} else {
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 $e) {
// ignoring exception
}
fclose($this->socket);
$this->socket = null;
}
return $result;
}
/**
* Get capabilities from IMAP server
*
* @return array list of capabilities
* @throws \Laminas\Mail\Protocol\Exception\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 \Laminas\Mail\Protocol\Exception\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 \Laminas\Mail\Protocol\Exception\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 \Laminas\Mail\Protocol\Exception\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
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 \Laminas\Mail\Protocol\Exception\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 \Laminas\Mail\Protocol\Exception\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 \Laminas\Mail\Protocol\Exception\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 $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 = (int) $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;
}
foreach ($response as $ids) {
if ($ids[0] == 'SEARCH') {
array_shift($ids);
return $ids;
}
}
return [];
}
}

View File

@@ -0,0 +1,401 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol;
use Laminas\Stdlib\ErrorHandler;
class Pop3
{
use ProtocolTrait;
/**
* Default timeout in seconds for initiating session
*/
const TIMEOUT_CONNECTION = 30;
/**
* saves if server supports top
* @var null|bool
*/
public $hasTop = null;
/**
* @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 = fputs($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)
{
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, ' ')) {
list($status, $message) = explode(' ', $result, 2);
} else {
$status = $result;
$message = '';
}
if ($status != '+OK') {
throw new Exception\RuntimeException('last request failed');
}
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;
}
/**
* 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 $e) {
// 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 $e) {
// 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');
list($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");
list(, $result) = explode(' ', $result);
return (int) $result;
}
$result = $this->request('LIST', true);
$messages = [];
$line = strtok($result, "\n");
while ($line) {
list($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");
list(, $result) = explode(' ', $result);
return $result;
}
$result = $this->request('UIDL', true);
$result = explode("\n", $result);
$messages = [];
foreach ($result as $line) {
if (! $line) {
continue;
}
list($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);
} else {
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)
{
$result = $this->request("RETR $msgno", true);
return $result;
}
/**
* 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 $msgno
*/
public function delete($msgno)
{
$this->request("DELE $msgno");
}
/**
* Make RSET call, which rollbacks delete requests
*/
public function undelete()
{
$this->request('RSET');
}
}

View File

@@ -0,0 +1,119 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol;
use Laminas\Stdlib\ErrorHandler;
/**
* 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?
*
* @return bool
*/
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

@@ -0,0 +1,460 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol;
/**
* 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;
/**
* 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 = null;
/**
* Whether or not send QUIT command
*
* @var bool
*/
protected $useCompleteQuit = true;
/**
* Constructor.
*
* 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;
}
/**
* 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|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 $e) {
$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
if (($fp = fopen("php://temp", "r+")) === false) {
throw new Exception\RuntimeException('cannot fopen');
}
if (fwrite($fp, $data) === false) {
throw new Exception\RuntimeException('cannot fwrite');
}
unset($data);
rewind($fp);
// max line length is 998 char + \r\n = 1000
while (($line = stream_get_line($fp, 1000, "\n")) !== false) {
$line = rtrim($line, "\r");
if (isset($line[0]) && $line[0] === '.') {
// Escape lines prefixed with a '.'
$line = '.' . $line;
}
$this->_send($line);
}
fclose($fp);
$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();
}
// @codingStandardsIgnoreStart
/**
* Disconnect from remote host and free resource
*/
protected function _disconnect()
{
// @codingStandardsIgnoreEnd
// 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

@@ -0,0 +1,138 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol\Smtp\Auth;
use Laminas\Crypt\Hmac;
use Laminas\Mail\Protocol\Smtp;
/**
* Performs CRAM-MD5 authentication
*/
class Crammd5 extends Smtp
{
/**
* @var string
*/
protected $username;
/**
* @var string
*/
protected $password;
/**
* Constructor.
*
* 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?
$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);
}
/**
* 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 string $username
* @return Crammd5
*/
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 Crammd5
*/
public function setPassword($password)
{
$this->password = $password;
return $this;
}
/**
* Get password
*
* @return string
*/
public function getPassword()
{
return $this->password;
}
/**
* Prepare CRAM-MD5 response to server's ticket
*
* @param string $key Challenge key (usually password)
* @param string $data Challenge data
* @param int $block Length of blocks (deprecated; unused)
* @return string
*/
protected function hmacMd5($key, $data, $block = 64)
{
return Hmac::compute($key, 'md5', $data);
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol\Smtp\Auth;
use Laminas\Mail\Protocol\Smtp;
/**
* Performs LOGIN authentication
*/
class Login extends Smtp
{
/**
* LOGIN username
*
* @var string
*/
protected $username;
/**
* LOGIN password
*
* @var string
*/
protected $password;
/**
* Constructor.
*
* @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

@@ -0,0 +1,124 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol\Smtp\Auth;
use Laminas\Mail\Protocol\Smtp;
/**
* Performs PLAIN authentication
*/
class Plain extends Smtp
{
/**
* PLAIN username
*
* @var string
*/
protected $username;
/**
* PLAIN password
*
* @var string
*/
protected $password;
/**
* Constructor.
*
* @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

@@ -0,0 +1,114 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol;
use Laminas\ServiceManager\AbstractPluginManager;
use Laminas\ServiceManager\Exception\InvalidServiceException;
use Laminas\ServiceManager\Factory\InvokableFactory;
/**
* Plugin manager implementation for SMTP extensions.
*
* Enforces that SMTP extensions retrieved are instances of Smtp. Additionally,
* it registers a number of default extensions available.
*/
class SmtpPluginManager extends AbstractPluginManager
{
/**
* Service aliases
*/
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,
'smtp' => Smtp::class,
'Smtp' => Smtp::class,
'SMTP' => Smtp::class,
// Legacy Zend Framework aliases
\Zend\Mail\Protocol\Smtp\Auth\Crammd5::class => Smtp\Auth\Crammd5::class,
\Zend\Mail\Protocol\Smtp\Auth\Login::class => Smtp\Auth\Login::class,
\Zend\Mail\Protocol\Smtp\Auth\Plain::class => Smtp\Auth\Plain::class,
\Zend\Mail\Protocol\Smtp::class => Smtp::class,
// v2 normalized FQCNs
'zendmailprotocolsmtpauthcrammd5' => Smtp\Auth\Crammd5::class,
'zendmailprotocolsmtpauthlogin' => Smtp\Auth\Login::class,
'zendmailprotocolsmtpauthplain' => Smtp\Auth\Plain::class,
'zendmailprotocolsmtp' => Smtp::class,
];
/**
* Service factories
*
* @var array
*/
protected $factories = [
Smtp\Auth\Crammd5::class => InvokableFactory::class,
Smtp\Auth\Login::class => InvokableFactory::class,
Smtp\Auth\Plain::class => InvokableFactory::class,
Smtp::class => InvokableFactory::class,
// v2 normalized service names
'laminasmailprotocolsmtpauthcrammd5' => InvokableFactory::class,
'laminasmailprotocolsmtpauthlogin' => InvokableFactory::class,
'laminasmailprotocolsmtpauthplain' => InvokableFactory::class,
'laminasmailprotocolsmtp' => InvokableFactory::class,
];
/**
* Plugins must be an instance of the Smtp class
*
* @var string
*/
protected $instanceOf = Smtp::class;
/**
* Validate a retrieved plugin instance (v3).
*
* @param object $plugin
* @throws InvalidServiceException
*/
public function validate($plugin)
{
if (! $plugin instanceof $this->instanceOf) {
throw new InvalidServiceException(sprintf(
'Plugin of type %s is invalid; must extend %s',
(is_object($plugin) ? get_class($plugin) : gettype($plugin)),
$this->instanceOf
));
}
}
/**
* Validate a retrieved plugin instance (v2).
*
* @param object $plugin
* @throws Exception\InvalidArgumentException
*/
public function validatePlugin($plugin)
{
try {
$this->validate($plugin);
} catch (InvalidServiceException $e) {
throw new Exception\InvalidArgumentException(
$e->getMessage(),
$e->getCode(),
$e
);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Protocol;
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\FactoryInterface;
use Laminas\ServiceManager\ServiceLocatorInterface;
class SmtpPluginManagerFactory implements FactoryInterface
{
/**
* laminas-servicemanager v2 support for invocation options.
*
* @param array
*/
protected $creationOptions;
/**
* {@inheritDoc}
*
* @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.
*
* @param array $options
* @return void
*/
public function setCreationOptions(array $options)
{
$this->creationOptions = $options;
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
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
const FLAG_PASSED = 'Passed';
const FLAG_SEEN = '\Seen';
const FLAG_UNSEEN = '\Unseen';
const FLAG_ANSWERED = '\Answered';
const FLAG_FLAGGED = '\Flagged';
const FLAG_DELETED = '\Deleted';
const FLAG_DRAFT = '\Draft';
const FLAG_RECENT = '\Recent';
}

View File

@@ -0,0 +1,319 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage;
use ArrayAccess;
use Countable;
use SeekableIterator;
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 = null;
/**
* used message class, change it in an extended class to extend the returned message class
* @var string
*/
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 bool supported or not
*/
public function __get($var)
{
if (strpos($var, 'has') === 0) {
$var = strtolower(substr($var, 3));
return isset($this->has[$var]) ? $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 $id int number of message
* @return Message
*/
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 $id
*/
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
*/
public function count()
{
return $this->countMessages();
}
/**
* ArrayAccess::offsetExists()
*
* @param int $id
* @return bool
*/
public function offsetExists($id)
{
try {
if ($this->getMessage($id)) {
return true;
}
} catch (Exception\ExceptionInterface $e) {
}
return false;
}
/**
* ArrayAccess::offsetGet()
*
* @param int $id
* @return \Laminas\Mail\Storage\Message message object
*/
public function offsetGet($id)
{
return $this->getMessage($id);
}
/**
* ArrayAccess::offsetSet()
*
* @param mixed $id
* @param mixed $value
* @throws Exception\RuntimeException
*/
public function offsetSet($id, $value)
{
throw new Exception\RuntimeException('cannot write mail messages via array access');
}
/**
* ArrayAccess::offsetUnset()
*
* @param int $id
* @return bool success
*/
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.
*/
public function rewind()
{
$this->iterationMax = $this->countMessages();
$this->iterationPos = 1;
}
/**
* Iterator::current()
*
* @return Message current message
*/
public function current()
{
return $this->getMessage($this->iterationPos);
}
/**
* Iterator::key()
*
* @return int id of current position
*/
public function key()
{
return $this->iterationPos;
}
/**
* Iterator::next()
*/
public function next()
{
++$this->iterationPos;
}
/**
* Iterator::valid()
*
* @return bool
*/
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
*/
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

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage\Exception;
use Laminas\Mail\Exception\ExceptionInterface as MailException;
interface ExceptionInterface extends MailException
{
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class OutOfBoundsException extends Exception\OutOfBoundsException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,209 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage;
use RecursiveIterator;
class Folder implements RecursiveIterator
{
/**
* subfolders of folder array(localName => \Laminas\Mail\Storage\Folder folder)
* @var array
*/
protected $folders;
/**
* local name (name of folder in parent folder)
* @var string
*/
protected $localName;
/**
* global name (absolute name of folder)
* @var string
*/
protected $globalName;
/**
* folder is selectable if folder is able to hold messages, otherwise it is a parent folder
* @var bool
*/
protected $selectable = true;
/**
* create a new mail folder instance
*
* @param string $localName name of folder in current subdirectory
* @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 $folders init with given instances of Folder as subfolders
*/
public function __construct($localName, $globalName = '', $selectable = true, array $folders = [])
{
$this->localName = $localName;
$this->globalName = $globalName ? $globalName : $localName;
$this->selectable = $selectable;
$this->folders = $folders;
}
/**
* implements RecursiveIterator::hasChildren()
*
* @return bool current element has children
*/
public function hasChildren()
{
$current = $this->current();
return $current && $current instanceof Folder && ! $current->isLeaf();
}
/**
* implements RecursiveIterator::getChildren()
*
* @return \Laminas\Mail\Storage\Folder same as self::current()
*/
public function getChildren()
{
return $this->current();
}
/**
* implements Iterator::valid()
*
* @return bool check if there's a current element
*/
public function valid()
{
return key($this->folders) !== null;
}
/**
* implements Iterator::next()
*/
public function next()
{
next($this->folders);
}
/**
* implements Iterator::key()
*
* @return string key/local name of current element
*/
public function key()
{
return key($this->folders);
}
/**
* implements Iterator::current()
*
* @return \Laminas\Mail\Storage\Folder current folder
*/
public function current()
{
return current($this->folders);
}
/**
* implements Iterator::rewind()
*/
public function rewind()
{
reset($this->folders);
}
/**
* get subfolder named $name
*
* @param string $name wanted subfolder
* @throws Exception\InvalidArgumentException
* @return \Laminas\Mail\Storage\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 \Laminas\Mail\Storage\Folder $folder instance for new subfolder
*/
public function __set($name, Folder $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()
{
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

@@ -0,0 +1,38 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage\Folder;
interface FolderInterface
{
/**
* get root folder or given folder
*
* @param string $rootFolder get folder structure for given folder, else root
* @return FolderInterface root or wanted folder
*/
public function getFolders($rootFolder = null);
/**
* select given folder
*
* folder must be selectable!
*
* @param FolderInterface|string $globalName global name of folder or instance for subfolder
* @throws \Laminas\Mail\Storage\Exception\ExceptionInterface
*/
public function selectFolder($globalName);
/**
* get Laminas\Mail\Storage\Folder instance for current folder
*
* @return FolderInterface instance of current folder
* @throws \Laminas\Mail\Storage\Exception\ExceptionInterface
*/
public function getCurrentFolder();
}

View File

@@ -0,0 +1,216 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage\Folder;
use Laminas\Mail\Storage;
use Laminas\Mail\Storage\Exception;
use Laminas\Stdlib\ErrorHandler;
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 $params array mail reader specific parameters
* @throws Exception\InvalidArgumentException
*/
public function __construct($params)
{
if (is_array($params)) {
$params = (object) $params;
}
if (! isset($params->dirname) || ! is_dir($params->dirname)) {
throw new Exception\InvalidArgumentException('no valid dirname given in params');
}
$this->rootdir = rtrim($params->dirname, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$this->delim = isset($params->delim) ? $params->delim : '.';
$this->buildFolderTree();
$this->selectFolder(! empty($params->folder) ? $params->folder : 'INBOX');
$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 (strpos($dir, $parent) === 0) {
$local = substr($dir, strlen($parent));
if (strpos($local, $this->delim) !== false) {
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 \Laminas\Mail\Storage\Exception\InvalidArgumentException
* @return \Laminas\Mail\Storage\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 (strpos($rootFolder, 'INBOX' . $this->delim) === 0) {
$rootFolder = substr($rootFolder, 6);
}
$currentFolder = $this->rootFolder;
$subname = trim($rootFolder, $this->delim);
while ($currentFolder) {
ErrorHandler::start(E_NOTICE);
list($entry, $subname) = explode($this->delim, $subname, 2);
ErrorHandler::stop();
$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 Storage\Folder instance of current folder
*/
public function getCurrentFolder()
{
return $this->currentFolder;
}
}

View File

@@ -0,0 +1,213 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage\Folder;
use Laminas\Mail\Storage;
use Laminas\Mail\Storage\Exception;
use Laminas\Stdlib\ErrorHandler;
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 $params array mail reader specific parameters
* @throws Exception\InvalidArgumentException
*/
public function __construct($params)
{
if (is_array($params)) {
$params = (object) $params;
}
if (isset($params->filename)) {
throw new Exception\InvalidArgumentException(sprintf('use %s for a single file', Storage\Mbox::class));
}
if (! isset($params->dirname) || ! is_dir($params->dirname)) {
throw new Exception\InvalidArgumentException('no valid dirname given in params');
}
$this->rootdir = rtrim($params->dirname, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$this->buildFolderTree($this->rootdir);
$this->selectFolder(! empty($params->folder) ? $params->folder : 'INBOX');
$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) {
ErrorHandler::start(E_NOTICE);
list($entry, $subname) = explode(DIRECTORY_SEPARATOR, $subname, 2);
ErrorHandler::stop();
$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 Storage\Folder 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

@@ -0,0 +1,551 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage;
use Laminas\Mail;
use Laminas\Mail\Protocol;
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[] = isset(static::$knownFlags[$flag]) ? 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)
* @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|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)
{
if (is_array($params)) {
$params = (object) $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;
}
if (! isset($params->user)) {
throw new Exception\InvalidArgumentException('need at least user in params');
}
$host = isset($params->host) ? $params->host : 'localhost';
$password = isset($params->password) ? $params->password : '';
$port = isset($params->port) ? $params->port : null;
$ssl = isset($params->ssl) ? $params->ssl : false;
$this->protocol = new Protocol\Imap();
if (isset($params->novalidatecert)) {
$this->protocol->setNoValidateCert((bool)$params->novalidatecert);
}
$this->protocol->connect($host, $port, $ssl);
if (! $this->protocol->login($params->user, $password)) {
throw new Exception\RuntimeException('cannot login, user or password wrong');
}
$this->selectFolder(isset($params->folder) ? $params->folder : 'INBOX');
}
/**
* 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 || strpos($globalName, $parent) === 0) {
$pos = strrpos($globalName, $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 = $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 Folder 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

@@ -0,0 +1,416 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage;
use Laminas\Mail;
use Laminas\Stdlib\ErrorHandler;
class Maildir extends AbstractStorage
{
/**
* used message class, change it in an extended class to extend the returned message class
* @var string
*/
protected $messageClass = Message\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 isset($filedata['size']) ? $filedata['size'] : filesize($filedata['filename']);
}
$result = [];
foreach ($this->files as $num => $data) {
$result[$num + 1] = isset($data['size']) ? $data['size'] : filesize($data['filename']);
}
return $result;
}
/**
* Fetch a message
*
* @param int $id number of message
* @return \Laminas\Mail\Storage\Message\File
* @throws \Laminas\Mail\Storage\Exception\ExceptionInterface
*/
public function getMessage($id)
{
// TODO that's ugly, would be better to let the message class decide
if (\trim($this->messageClass, '\\') === Message\File::class
|| is_subclass_of($this->messageClass, Message\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 $params array mail reader specific parameters
* @throws Exception\InvalidArgumentException
*/
public function __construct($params)
{
if (is_array($params)) {
$params = (object) $params;
}
if (! isset($params->dirname) || ! is_dir($params->dirname)) {
throw new Exception\InvalidArgumentException('no valid dirname given in params');
}
if (! $this->isMaildir($params->dirname)) {
throw new Exception\InvalidArgumentException('invalid maildir given');
}
$this->has['top'] = true;
$this->has['flags'] = true;
$this->openMaildir($params->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;
}
ErrorHandler::start(E_NOTICE);
list($uniq, $info) = explode(':', $entry, 2);
list(, $size) = explode(',', $uniq, 2);
ErrorHandler::stop();
if ($size && $size[0] == 'S' && $size[1] == '=') {
$size = substr($size, 2);
}
if (! ctype_digit($size)) {
$size = null;
}
ErrorHandler::start(E_NOTICE);
list($version, $flags) = explode(',', $info, 2);
ErrorHandler::stop();
if ($version != 2) {
$flags = '';
}
$namedFlags = $defaultFlags;
$length = strlen($flags);
for ($i = 0; $i < $length; ++$i) {
$flag = $flags[$i];
$namedFlags[$flag] = isset(static::$knownFlags[$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, function ($a, $b) {
return \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 $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

@@ -0,0 +1,415 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage;
use Laminas\Stdlib\ErrorHandler;
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 string
*/
protected $messageClass = Message\File::class;
/**
* end of Line for messages
*
* @var string|null
*/
protected $messageEOL;
/**
* Count messages all messages in current box
*
* @return int number of messages
* @throws \Laminas\Mail\Storage\Exception\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 \Laminas\Mail\Storage\Message\File
* @throws \Laminas\Mail\Storage\Exception\ExceptionInterface
*/
public function getMessage($id)
{
// TODO that's ugly, would be better to let the message class decide
if (is_subclass_of($this->messageClass, Message\File::class)
|| strtolower($this->messageClass) === strtolower(Message\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);
}
$bodyLines = 0; // TODO: need a way to change that
$message = $this->getRawHeader($id);
// file pointer is after headers now
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 \Laminas\Mail\Storage\Exception\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 \Laminas\Mail\Storage\Exception\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 $params array mail reader specific parameters
* @throws Exception\InvalidArgumentException
*/
public function __construct($params)
{
if (is_array($params)) {
$params = (object) $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($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 (strpos($line, 'From ') === 0) {
$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 (strpos($line, 'From ') === 0) {
$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()
{
ErrorHandler::start(E_WARNING);
fclose($this->fh);
ErrorHandler::stop();
$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 $id
* @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 \Laminas\Mail\Storage\Exception\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 \Laminas\Mail\Storage\Exception\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

@@ -0,0 +1,86 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage;
use Laminas\Stdlib\ErrorHandler;
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

@@ -0,0 +1,70 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage\Message;
use Laminas\Mail\Storage\Part;
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 \Laminas\Mail\Storage\Exception\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

@@ -0,0 +1,34 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
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

@@ -0,0 +1,474 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage;
use Laminas\Mail\Header\HeaderInterface;
use Laminas\Mail\Headers;
use Laminas\Mime;
use RecursiveIterator;
class Part implements RecursiveIterator, Part\PartInterface
{
/**
* 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'] = isset($params['strict']) ? $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 $e) {
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 null
*/
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 = '';
foreach ($header as $h) {
$return .= $h->getFieldValue(HeaderInterface::FORMAT_RAW)
. Mime\Mime::LINEEND;
}
$return = trim($return, 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 \Laminas\Mime\Exception\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
* @return bool
*/
public function __isset($name)
{
return $this->getHeaders()->has($name);
}
/**
* magic method to get content of part
*
* @return string content
*/
public function __toString()
{
return $this->getContent();
}
/**
* implements RecursiveIterator::hasChildren()
*
* @return bool current element has children/is multipart
*/
public function hasChildren()
{
$current = $this->current();
return $current && $current instanceof Part && $current->isMultipart();
}
/**
* implements RecursiveIterator::getChildren()
*
* @return Part same as self::current()
*/
public function getChildren()
{
return $this->current();
}
/**
* implements Iterator::valid()
*
* @return bool check if there's a current element
*/
public function valid()
{
if ($this->countParts === null) {
$this->countParts();
}
return $this->iterationPos && $this->iterationPos <= $this->countParts;
}
/**
* implements Iterator::next()
*/
public function next()
{
++$this->iterationPos;
}
/**
* implements Iterator::key()
*
* @return string key/number of current part
*/
public function key()
{
return $this->iterationPos;
}
/**
* implements Iterator::current()
*
* @return Part current part
*/
public function current()
{
return $this->getPart($this->iterationPos);
}
/**
* implements Iterator::rewind()
*/
public function rewind()
{
$this->countParts();
$this->iterationPos = 1;
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage\Part\Exception;
use Laminas\Mail\Storage\Exception\ExceptionInterface as StorageException;
interface ExceptionInterface extends StorageException
{
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
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

@@ -0,0 +1,18 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
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

@@ -0,0 +1,157 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage\Part;
use Laminas\Mail\Headers;
use Laminas\Mail\Storage\Part;
class File extends Part
{
protected $contentPos = [];
protected $partPos = [];
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'])) {
$this->fh = fopen($params['file'], 'r');
} else {
$this->fh = $params['file'];
}
if (! $this->fh) {
throw new Exception\RuntimeException('could not open file');
}
if (isset($params['startPos'])) {
fseek($this->fh, $params['startPos']);
}
$header = '';
$endPos = isset($params['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

@@ -0,0 +1,123 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
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

@@ -0,0 +1,283 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage;
use Laminas\Mail\Exception as MailException;
use Laminas\Mail\Protocol;
use Laminas\Mime;
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 \Laminas\Mail\Storage\Exception\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 ? $id : null;
return $this->protocol->getList($id);
}
/**
* Fetch a message
*
* @param int $id number of message
* @return \Laminas\Mail\Storage\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 \Laminas\Mail\Storage\Exception\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 \Laminas\Mail\Storage\Exception\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|Protocol\Pop3 $params mail reader specific parameters or configured Pop3 protocol object
* @throws \Laminas\Mail\Storage\Exception\InvalidArgumentException
* @throws \Laminas\Mail\Protocol\Exception\RuntimeException
*/
public function __construct($params)
{
if (is_array($params)) {
$params = (object) $params;
}
$this->has['fetchPart'] = false;
$this->has['top'] = null;
$this->has['uniqueid'] = null;
if ($params instanceof Protocol\Pop3) {
$this->protocol = $params;
return;
}
if (! isset($params->user)) {
throw new Exception\InvalidArgumentException('need at least user in params');
}
$host = isset($params->host) ? $params->host : 'localhost';
$password = isset($params->password) ? $params->password : '';
$port = isset($params->port) ? $params->port : null;
$ssl = isset($params->ssl) ? $params->ssl : false;
$this->protocol = new Protocol\Pop3();
if (isset($params->novalidatecert)) {
$this->protocol->setNoValidateCert((bool)$params->novalidatecert);
}
$this->protocol->connect($host, $port, $ssl);
$this->protocol->login($params->user, $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 \Laminas\Mail\Protocol\Exception\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 \Laminas\Mail\Protocol\Exception\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 \Laminas\Mail\Storage\Exception\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 Exception\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 Exception\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 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 $e) {
// 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 $e) {
// ignoring error
}
$this->has['uniqueid'] = (bool) $id;
return $this->has['uniqueid'];
}
return $result;
}
}

View File

@@ -0,0 +1,953 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Storage\Writable;
use Laminas\Mail\Exception as MailException;
use Laminas\Mail\Storage;
use Laminas\Mail\Storage\Exception as StorageException;
use Laminas\Mail\Storage\Folder;
use Laminas\Stdlib\ErrorHandler;
use RecursiveIteratorIterator;
class Maildir extends Folder\Maildir implements WritableInterface
{
// TODO: init maildir (+ constructor option create if not found)
/**
* use quota and size of quota if given
*
* @var bool|int
*/
protected $quota;
/**
* create a new maildir
*
* If the given dir is already a valid maildir this will not fail.
*
* @param string $dir directory for the new maildir (may already exist)
* @throws \Laminas\Mail\Storage\Exception\RuntimeException
* @throws \Laminas\Mail\Storage\Exception\InvalidArgumentException
*/
public static function initMaildir($dir)
{
if (file_exists($dir)) {
if (! is_dir($dir)) {
throw new StorageException\InvalidArgumentException('maildir must be a directory if already exists');
}
} else {
ErrorHandler::start();
$test = mkdir($dir);
$error = ErrorHandler::stop();
if (! $test) {
$dir = dirname($dir);
if (! file_exists($dir)) {
throw new StorageException\InvalidArgumentException("parent $dir not found", 0, $error);
} elseif (! is_dir($dir)) {
throw new StorageException\InvalidArgumentException("parent $dir not a directory", 0, $error);
} else {
throw new StorageException\RuntimeException('cannot create maildir', 0, $error);
}
}
}
foreach (['cur', 'tmp', 'new'] as $subdir) {
ErrorHandler::start();
$test = mkdir($dir . DIRECTORY_SEPARATOR . $subdir);
$error = ErrorHandler::stop();
if (! $test) {
// ignore if dir exists (i.e. was already valid maildir or two processes try to create one)
if (! file_exists($dir . DIRECTORY_SEPARATOR . $subdir)) {
throw new StorageException\RuntimeException('could not create subdir ' . $subdir, 0, $error);
}
}
}
}
/**
* Create instance with parameters
* Additional parameters are (see parent for more):
* - create if true a new maildir is create if none exists
*
* @param $params array mail reader specific parameters
* @throws \Laminas\Mail\Storage\Exception\ExceptionInterface
*/
public function __construct($params)
{
if (is_array($params)) {
$params = (object) $params;
}
if (! empty($params->create)
&& isset($params->dirname)
&& ! file_exists($params->dirname . DIRECTORY_SEPARATOR . 'cur')
) {
self::initMaildir($params->dirname);
}
parent::__construct($params);
}
/**
* 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|\Laminas\Mail\Storage\Folder $parentFolder parent of new folder, else root folder is parent
* @throws \Laminas\Mail\Storage\Exception\RuntimeException
* @return string only used internally (new created maildir)
*/
public function createFolder($name, $parentFolder = null)
{
if ($parentFolder instanceof Folder) {
$folder = $parentFolder->getGlobalName() . $this->delim . $name;
} elseif ($parentFolder !== null) {
$folder = rtrim($parentFolder, $this->delim) . $this->delim . $name;
} else {
$folder = $name;
}
$folder = trim($folder, $this->delim);
// first we check if we try to create a folder that does exist
$exists = null;
try {
$exists = $this->getFolders($folder);
} catch (MailException\ExceptionInterface $e) {
// ok
}
if ($exists) {
throw new StorageException\RuntimeException('folder already exists');
}
if (strpos($folder, $this->delim . $this->delim) !== false) {
throw new StorageException\RuntimeException('invalid name - folder parts may not be empty');
}
if (strpos($folder, 'INBOX' . $this->delim) === 0) {
$folder = substr($folder, 6);
}
$fulldir = $this->rootdir . '.' . $folder;
// check if we got tricked and would create a dir outside of the rootdir or not as direct child
if (strpos($folder, DIRECTORY_SEPARATOR) !== false || strpos($folder, '/') !== false
|| dirname($fulldir) . DIRECTORY_SEPARATOR != $this->rootdir
) {
throw new StorageException\RuntimeException('invalid name - no directory separator allowed in folder name');
}
// has a parent folder?
$parent = null;
if (strpos($folder, $this->delim)) {
// let's see if the parent folder exists
$parent = substr($folder, 0, strrpos($folder, $this->delim));
try {
$this->getFolders($parent);
} catch (MailException\ExceptionInterface $e) {
// does not - create parent folder
$this->createFolder($parent);
}
}
ErrorHandler::start();
if (! mkdir($fulldir) || ! mkdir($fulldir . DIRECTORY_SEPARATOR . 'cur')) {
$error = ErrorHandler::stop();
throw new StorageException\RuntimeException(
'error while creating new folder, may be created incompletely',
0,
$error
);
}
ErrorHandler::stop();
mkdir($fulldir . DIRECTORY_SEPARATOR . 'new');
mkdir($fulldir . DIRECTORY_SEPARATOR . 'tmp');
$localName = $parent ? substr($folder, strlen($parent) + 1) : $folder;
$this->getFolders($parent)->$localName = new Folder($localName, $folder, true);
return $fulldir;
}
/**
* remove a folder
*
* @param string|Folder $name name or instance of folder
* @throws \Laminas\Mail\Storage\Exception\RuntimeException
*/
public function removeFolder($name)
{
// TODO: This could fail in the middle of the task, which is not optimal.
// But there is no defined standard way to mark a folder as removed and there is no atomar fs-op
// to remove a directory. Also moving the folder to a/the trash folder is not possible, as
// all parent folders must be created. What we could do is add a dash to the front of the
// directory name and it should be ignored as long as other processes obey the standard.
if ($name instanceof Folder) {
$name = $name->getGlobalName();
}
$name = trim($name, $this->delim);
if (strpos($name, 'INBOX' . $this->delim) === 0) {
$name = substr($name, 6);
}
// check if folder exists and has no children
if (! $this->getFolders($name)->isLeaf()) {
throw new StorageException\RuntimeException('delete children first');
}
if ($name == 'INBOX' || $name == DIRECTORY_SEPARATOR || $name == '/') {
throw new StorageException\RuntimeException('wont delete INBOX');
}
if ($name == $this->getCurrentFolder()) {
throw new StorageException\RuntimeException('wont delete selected folder');
}
foreach (['tmp', 'new', 'cur', '.'] as $subdir) {
$dir = $this->rootdir . '.' . $name . DIRECTORY_SEPARATOR . $subdir;
if (! file_exists($dir)) {
continue;
}
$dh = opendir($dir);
if (! $dh) {
throw new StorageException\RuntimeException("error opening $subdir");
}
while (($entry = readdir($dh)) !== false) {
if ($entry == '.' || $entry == '..') {
continue;
}
if (! unlink($dir . DIRECTORY_SEPARATOR . $entry)) {
throw new StorageException\RuntimeException("error cleaning $subdir");
}
}
closedir($dh);
if ($subdir !== '.') {
if (! rmdir($dir)) {
throw new StorageException\RuntimeException("error removing $subdir");
}
}
}
if (! rmdir($this->rootdir . '.' . $name)) {
// at least we should try to make it a valid maildir again
mkdir($this->rootdir . '.' . $name . DIRECTORY_SEPARATOR . 'cur');
throw new StorageException\RuntimeException("error removing maindir");
}
$parent = strpos($name, $this->delim) ? substr($name, 0, strrpos($name, $this->delim)) : null;
$localName = $parent ? substr($name, strlen($parent) + 1) : $name;
unset($this->getFolders($parent)->$localName);
}
/**
* rename and/or move folder
*
* The new name has the same restrictions as in createFolder()
*
* @param string|\Laminas\Mail\Storage\Folder $oldName name or instance of folder
* @param string $newName new global name of folder
* @throws \Laminas\Mail\Storage\Exception\RuntimeException
*/
public function renameFolder($oldName, $newName)
{
// TODO: This is also not atomar and has similar problems as removeFolder()
if ($oldName instanceof Folder) {
$oldName = $oldName->getGlobalName();
}
$oldName = trim($oldName, $this->delim);
if (strpos($oldName, 'INBOX' . $this->delim) === 0) {
$oldName = substr($oldName, 6);
}
$newName = trim($newName, $this->delim);
if (strpos($newName, 'INBOX' . $this->delim) === 0) {
$newName = substr($newName, 6);
}
if (strpos($newName, $oldName . $this->delim) === 0) {
throw new StorageException\RuntimeException('new folder cannot be a child of old folder');
}
// check if folder exists and has no children
$folder = $this->getFolders($oldName);
if ($oldName == 'INBOX' || $oldName == DIRECTORY_SEPARATOR || $oldName == '/') {
throw new StorageException\RuntimeException('wont rename INBOX');
}
if ($oldName == $this->getCurrentFolder()) {
throw new StorageException\RuntimeException('wont rename selected folder');
}
$newdir = $this->createFolder($newName);
if (! $folder->isLeaf()) {
foreach ($folder as $k => $v) {
$this->renameFolder($v->getGlobalName(), $newName . $this->delim . $k);
}
}
$olddir = $this->rootdir . '.' . $folder;
foreach (['tmp', 'new', 'cur'] as $subdir) {
$subdir = DIRECTORY_SEPARATOR . $subdir;
if (! file_exists($olddir . $subdir)) {
continue;
}
// using copy or moving files would be even better - but also much slower
if (! rename($olddir . $subdir, $newdir . $subdir)) {
throw new StorageException\RuntimeException('error while moving ' . $subdir);
}
}
// create a dummy if removing fails - otherwise we can't read it next time
mkdir($olddir . DIRECTORY_SEPARATOR . 'cur');
$this->removeFolder($oldName);
}
/**
* create a uniqueid for maildir filename
*
* This is nearly the format defined in the maildir standard. The microtime() call should already
* create a uniqueid, the pid is for multicore/-cpu machine that manage to call this function at the
* exact same time, and uname() gives us the hostname for multiple machines accessing the same storage.
*
* If someone disables posix we create a random number of the same size, so this method should also
* work on Windows - if you manage to get maildir working on Windows.
* Microtime could also be disabled, although I've never seen it.
*
* @return string new uniqueid
*/
protected function createUniqueId()
{
$id = '';
$id .= microtime(true);
$id .= '.' . getmypid();
$id .= '.' . php_uname('n');
return $id;
}
/**
* open a temporary maildir file
*
* makes sure tmp/ exists and create a file with a unique name
* you should close the returned filehandle!
*
* @param string $folder name of current folder without leading .
* @throws \Laminas\Mail\Storage\Exception\RuntimeException
* @return array array('dirname' => dir of maildir folder, 'uniq' => unique id, 'filename' => name of create file
* 'handle' => file opened for writing)
*/
protected function createTmpFile($folder = 'INBOX')
{
if ($folder == 'INBOX') {
$tmpdir = $this->rootdir . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
} else {
$tmpdir = $this->rootdir . '.' . $folder . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
}
if (! file_exists($tmpdir)) {
if (! mkdir($tmpdir)) {
throw new StorageException\RuntimeException('problems creating tmp dir');
}
}
// we should retry to create a unique id if a file with the same name exists
// to avoid a script timeout we only wait 1 second (instead of 2) and stop
// after a defined retry count
// if you change this variable take into account that it can take up to $maxTries seconds
// normally we should have a valid unique name after the first try, we're just following the "standard" here
$maxTries = 5;
for ($i = 0; $i < $maxTries; ++$i) {
$uniq = $this->createUniqueId();
if (! file_exists($tmpdir . $uniq)) {
// here is the race condition! - as defined in the standard
// to avoid having a long time between stat()ing the file and creating it we're opening it here
// to mark the filename as taken
$fh = fopen($tmpdir . $uniq, 'w');
if (! $fh) {
throw new StorageException\RuntimeException('could not open temp file');
}
break;
}
sleep(1);
}
if (! $fh) {
throw new StorageException\RuntimeException(
"tried {$maxTries} unique ids for a temp file, but all were taken - giving up"
);
}
return ['dirname' => $this->rootdir . '.' . $folder,
'uniq' => $uniq,
'filename' => $tmpdir . $uniq,
'handle' => $fh];
}
/**
* create an info string for filenames with given flags
*
* @param array $flags wanted flags, with the reference you'll get the set
* flags with correct key (= char for flag)
* @return string info string for version 2 filenames including the leading colon
* @throws StorageException\InvalidArgumentException
*/
protected function getInfoString(&$flags)
{
// accessing keys is easier, faster and it removes duplicated flags
$wantedFlags = array_flip($flags);
if (isset($wantedFlags[Storage::FLAG_RECENT])) {
throw new StorageException\InvalidArgumentException('recent flag may not be set');
}
$info = ':2,';
$flags = [];
foreach (Storage\Maildir::$knownFlags as $char => $flag) {
if (! isset($wantedFlags[$flag])) {
continue;
}
$info .= $char;
$flags[$char] = $flag;
unset($wantedFlags[$flag]);
}
if (! empty($wantedFlags)) {
$wantedFlags = implode(', ', array_keys($wantedFlags));
throw new StorageException\InvalidArgumentException('unknown flag(s): ' . $wantedFlags);
}
return $info;
}
/**
* append a new message to mail storage
*
* @param string|resource $message message as string or stream resource.
* @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.
* @param bool $recent handle this mail as if recent flag has been set,
* should only be used in delivery.
* @throws StorageException\RuntimeException
*/
public function appendMessage($message, $folder = null, $flags = null, $recent = false)
{
if ($this->quota && $this->checkQuota()) {
throw new StorageException\RuntimeException('storage is over quota!');
}
if ($folder === null) {
$folder = $this->currentFolder;
}
if (! ($folder instanceof Folder)) {
$folder = $this->getFolders($folder);
}
if ($flags === null) {
$flags = [Storage::FLAG_SEEN];
}
$info = $this->getInfoString($flags);
$tempFile = $this->createTmpFile($folder->getGlobalName());
// TODO: handle class instances for $message
if (is_resource($message) && get_resource_type($message) == 'stream') {
stream_copy_to_stream($message, $tempFile['handle']);
} else {
fwrite($tempFile['handle'], $message);
}
fclose($tempFile['handle']);
// we're adding the size to the filename for maildir++
$size = filesize($tempFile['filename']);
if ($size !== false) {
$info = ',S=' . $size . $info;
}
$newFilename = $tempFile['dirname'] . DIRECTORY_SEPARATOR;
$newFilename .= $recent ? 'new' : 'cur';
$newFilename .= DIRECTORY_SEPARATOR . $tempFile['uniq'] . $info;
// we're throwing any exception after removing our temp file and saving it to this variable instead
$exception = null;
if (! link($tempFile['filename'], $newFilename)) {
$exception = new StorageException\RuntimeException('cannot link message file to final dir');
}
ErrorHandler::start(E_WARNING);
unlink($tempFile['filename']);
ErrorHandler::stop();
if ($exception) {
throw $exception;
}
$this->files[] = ['uniq' => $tempFile['uniq'],
'flags' => $flags,
'filename' => $newFilename];
if ($this->quota) {
$this->addQuotaEntry((int) $size, 1);
}
}
/**
* copy an existing message
*
* @param int $id number of message
* @param string|\Laminas\Mail\Storage\Folder $folder name or instance of targer folder
* @throws \Laminas\Mail\Storage\Exception\RuntimeException
*/
public function copyMessage($id, $folder)
{
if ($this->quota && $this->checkQuota()) {
throw new StorageException\RuntimeException('storage is over quota!');
}
if (! ($folder instanceof Folder)) {
$folder = $this->getFolders($folder);
}
$filedata = $this->getFileData($id);
$oldFile = $filedata['filename'];
$flags = $filedata['flags'];
// copied message can't be recent
while (($key = array_search(Storage::FLAG_RECENT, $flags)) !== false) {
unset($flags[$key]);
}
$info = $this->getInfoString($flags);
// we're creating the copy as temp file before moving to cur/
$tempFile = $this->createTmpFile($folder->getGlobalName());
// we don't write directly to the file
fclose($tempFile['handle']);
// we're adding the size to the filename for maildir++
$size = filesize($oldFile);
if ($size !== false) {
$info = ',S=' . $size . $info;
}
$newFile = $tempFile['dirname'] . DIRECTORY_SEPARATOR . 'cur' . DIRECTORY_SEPARATOR . $tempFile['uniq'] . $info;
// we're throwing any exception after removing our temp file and saving it to this variable instead
$exception = null;
if (! copy($oldFile, $tempFile['filename'])) {
$exception = new StorageException\RuntimeException('cannot copy message file');
} elseif (! link($tempFile['filename'], $newFile)) {
$exception = new StorageException\RuntimeException('cannot link message file to final dir');
}
ErrorHandler::start(E_WARNING);
unlink($tempFile['filename']);
ErrorHandler::stop();
if ($exception) {
throw $exception;
}
if ($folder->getGlobalName() == $this->currentFolder
|| ($this->currentFolder == 'INBOX' && $folder->getGlobalName() == '/')
) {
$this->files[] = ['uniq' => $tempFile['uniq'],
'flags' => $flags,
'filename' => $newFile];
}
if ($this->quota) {
$this->addQuotaEntry((int) $size, 1);
}
}
/**
* move an existing message
*
* @param int $id number of message
* @param string|\Laminas\Mail\Storage\Folder $folder name or instance of targer folder
* @throws \Laminas\Mail\Storage\Exception\RuntimeException
*/
public function moveMessage($id, $folder)
{
if (! ($folder instanceof Folder)) {
$folder = $this->getFolders($folder);
}
if ($folder->getGlobalName() == $this->currentFolder
|| ($this->currentFolder == 'INBOX' && $folder->getGlobalName() == '/')
) {
throw new StorageException\RuntimeException('target is current folder');
}
$filedata = $this->getFileData($id);
$oldFile = $filedata['filename'];
$flags = $filedata['flags'];
// moved message can't be recent
while (($key = array_search(Storage::FLAG_RECENT, $flags)) !== false) {
unset($flags[$key]);
}
$info = $this->getInfoString($flags);
// reserving a new name
$tempFile = $this->createTmpFile($folder->getGlobalName());
fclose($tempFile['handle']);
// we're adding the size to the filename for maildir++
$size = filesize($oldFile);
if ($size !== false) {
$info = ',S=' . $size . $info;
}
$newFile = $tempFile['dirname'] . DIRECTORY_SEPARATOR . 'cur' . DIRECTORY_SEPARATOR . $tempFile['uniq'] . $info;
// we're throwing any exception after removing our temp file and saving it to this variable instead
$exception = null;
if (! rename($oldFile, $newFile)) {
$exception = new StorageException\RuntimeException('cannot move message file');
}
ErrorHandler::start(E_WARNING);
unlink($tempFile['filename']);
ErrorHandler::stop();
if ($exception) {
throw $exception;
}
unset($this->files[$id - 1]);
// remove the gap
$this->files = array_values($this->files);
}
/**
* 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 \Laminas\Mail\Storage\Exception\RuntimeException
*/
public function setFlags($id, $flags)
{
$info = $this->getInfoString($flags);
$filedata = $this->getFileData($id);
// NOTE: double dirname to make sure we always move to cur. if recent
// flag has been set (message is in new) it will be moved to cur.
$newFilename = dirname(dirname($filedata['filename']))
. DIRECTORY_SEPARATOR
. 'cur'
. DIRECTORY_SEPARATOR
. "$filedata[uniq]$info";
ErrorHandler::start();
$test = rename($filedata['filename'], $newFilename);
$error = ErrorHandler::stop();
if (! $test) {
throw new StorageException\RuntimeException('cannot rename file', 0, $error);
}
$filedata['flags'] = $flags;
$filedata['filename'] = $newFilename;
$this->files[$id - 1] = $filedata;
}
/**
* stub for not supported message deletion
*
* @param $id
* @throws \Laminas\Mail\Storage\Exception\RuntimeException
*/
public function removeMessage($id)
{
$filename = $this->getFileData($id, 'filename');
if ($this->quota) {
$size = filesize($filename);
}
ErrorHandler::start();
$test = unlink($filename);
$error = ErrorHandler::stop();
if (! $test) {
throw new StorageException\RuntimeException('cannot remove message', 0, $error);
}
unset($this->files[$id - 1]);
// remove the gap
$this->files = array_values($this->files);
if ($this->quota) {
$this->addQuotaEntry(0 - (int) $size, -1);
}
}
/**
* enable/disable quota and set a quota value if wanted or needed
*
* You can enable/disable quota with true/false. If you don't have
* a MDA or want to enforce a quota value you can also set this value
* here. Use array('size' => SIZE_QUOTA, 'count' => MAX_MESSAGE) do
* define your quota. Order of these fields does matter!
*
* @param bool|array $value new quota value
*/
public function setQuota($value)
{
$this->quota = $value;
}
/**
* get currently set quota
*
* @see \Laminas\Mail\Storage\Writable\Maildir::setQuota()
* @param bool $fromStorage
* @throws \Laminas\Mail\Storage\Exception\RuntimeException
* @return bool|array
*/
public function getQuota($fromStorage = false)
{
if ($fromStorage) {
ErrorHandler::start(E_WARNING);
$fh = fopen($this->rootdir . 'maildirsize', 'r');
$error = ErrorHandler::stop();
if (! $fh) {
throw new StorageException\RuntimeException('cannot open maildirsize', 0, $error);
}
$definition = fgets($fh);
fclose($fh);
$definition = explode(',', trim($definition));
$quota = [];
foreach ($definition as $member) {
$key = $member[strlen($member) - 1];
if ($key == 'S' || $key == 'C') {
$key = $key == 'C' ? 'count' : 'size';
}
$quota[$key] = substr($member, 0, -1);
}
return $quota;
}
return $this->quota;
}
/**
* @see http://www.inter7.com/courierimap/README.maildirquota.html "Calculating maildirsize"
* @throws \Laminas\Mail\Storage\Exception\RuntimeException
* @return array
*/
protected function calculateMaildirsize()
{
$timestamps = [];
$messages = 0;
$totalSize = 0;
if (is_array($this->quota)) {
$quota = $this->quota;
} else {
try {
$quota = $this->getQuota(true);
} catch (StorageException\ExceptionInterface $e) {
throw new StorageException\RuntimeException('no quota definition found', 0, $e);
}
}
$folders = new RecursiveIteratorIterator($this->getFolders(), RecursiveIteratorIterator::SELF_FIRST);
foreach ($folders as $folder) {
$subdir = $folder->getGlobalName();
if ($subdir == 'INBOX') {
$subdir = '';
} else {
$subdir = '.' . $subdir;
}
if ($subdir == 'Trash') {
continue;
}
foreach (['cur', 'new'] as $subsubdir) {
$dirname = $this->rootdir . $subdir . DIRECTORY_SEPARATOR . $subsubdir . DIRECTORY_SEPARATOR;
if (! file_exists($dirname)) {
continue;
}
// NOTE: we are using mtime instead of "the latest timestamp". The latest would be atime
// and as we are accessing the directory it would make the whole calculation useless.
$timestamps[$dirname] = filemtime($dirname);
$dh = opendir($dirname);
// NOTE: Should have been checked in constructor. Not throwing an exception here, quotas will
// therefore not be fully enforced, but next request will fail anyway, if problem persists.
if (! $dh) {
continue;
}
while (($entry = readdir()) !== false) {
if ($entry[0] == '.' || ! is_file($dirname . $entry)) {
continue;
}
if (strpos($entry, ',S=')) {
strtok($entry, '=');
$filesize = strtok(':');
if (is_numeric($filesize)) {
$totalSize += $filesize;
++$messages;
continue;
}
}
$size = filesize($dirname . $entry);
if ($size === false) {
// ignore, as we assume file got removed
continue;
}
$totalSize += $size;
++$messages;
}
}
}
$tmp = $this->createTmpFile();
$fh = $tmp['handle'];
$definition = [];
foreach ($quota as $type => $value) {
if ($type == 'size' || $type == 'count') {
$type = $type == 'count' ? 'C' : 'S';
}
$definition[] = $value . $type;
}
$definition = implode(',', $definition);
fwrite($fh, "$definition\n");
fwrite($fh, "$totalSize $messages\n");
fclose($fh);
rename($tmp['filename'], $this->rootdir . 'maildirsize');
foreach ($timestamps as $dir => $timestamp) {
if ($timestamp < filemtime($dir)) {
unlink($this->rootdir . 'maildirsize');
break;
}
}
return ['size' => $totalSize,
'count' => $messages,
'quota' => $quota];
}
/**
* @see http://www.inter7.com/courierimap/README.maildirquota.html "Calculating the quota for a Maildir++"
* @param bool $forceRecalc
* @return array
*/
protected function calculateQuota($forceRecalc = false)
{
$fh = null;
$totalSize = 0;
$messages = 0;
$maildirsize = '';
if (! $forceRecalc
&& file_exists($this->rootdir . 'maildirsize')
&& filesize($this->rootdir . 'maildirsize') < 5120
) {
$fh = fopen($this->rootdir . 'maildirsize', 'r');
}
if ($fh) {
$maildirsize = fread($fh, 5120);
if (strlen($maildirsize) >= 5120) {
fclose($fh);
$fh = null;
$maildirsize = '';
}
}
if (! $fh) {
$result = $this->calculateMaildirsize();
$totalSize = $result['size'];
$messages = $result['count'];
$quota = $result['quota'];
} else {
$maildirsize = explode("\n", $maildirsize);
if (is_array($this->quota)) {
$quota = $this->quota;
} else {
$definition = explode(',', $maildirsize[0]);
$quota = [];
foreach ($definition as $member) {
$key = $member[strlen($member) - 1];
if ($key == 'S' || $key == 'C') {
$key = $key == 'C' ? 'count' : 'size';
}
$quota[$key] = substr($member, 0, -1);
}
}
unset($maildirsize[0]);
foreach ($maildirsize as $line) {
list($size, $count) = explode(' ', trim($line));
$totalSize += $size;
$messages += $count;
}
}
$overQuota = false;
$overQuota = $overQuota || (isset($quota['size']) && $totalSize > $quota['size']);
$overQuota = $overQuota || (isset($quota['count']) && $messages > $quota['count']);
// NOTE: $maildirsize equals false if it wasn't set (AKA we recalculated) or it's only
// one line, because $maildirsize[0] gets unsetted.
// Also we're using local time to calculate the 15 minute offset. Touching a file just for known the
// local time of the file storage isn't worth the hassle.
if ($overQuota && ($maildirsize || filemtime($this->rootdir . 'maildirsize') > time() - 900)) {
$result = $this->calculateMaildirsize();
$totalSize = $result['size'];
$messages = $result['count'];
$quota = $result['quota'];
$overQuota = false;
$overQuota = $overQuota || (isset($quota['size']) && $totalSize > $quota['size']);
$overQuota = $overQuota || (isset($quota['count']) && $messages > $quota['count']);
}
if ($fh) {
// TODO is there a safe way to keep the handle open for writing?
fclose($fh);
}
return ['size' => $totalSize,
'count' => $messages,
'quota' => $quota,
'over_quota' => $overQuota];
}
protected function addQuotaEntry($size, $count = 1)
{
if (! file_exists($this->rootdir . 'maildirsize')) {
// TODO: should get file handler from calculateQuota
}
$size = (int) $size;
$count = (int) $count;
file_put_contents($this->rootdir . 'maildirsize', "$size $count\n", FILE_APPEND);
}
/**
* check if storage is currently over quota
*
* @see calculateQuota()
* @param bool $detailedResponse return known data of quota and current size and message count
* @param bool $forceRecalc
* @return bool|array over quota state or detailed response
*/
public function checkQuota($detailedResponse = false, $forceRecalc = false)
{
$result = $this->calculateQuota($forceRecalc);
return $detailedResponse ? $result : $result['over_quota'];
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
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

@@ -0,0 +1,64 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport;
use Laminas\Stdlib\AbstractOptions;
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

@@ -0,0 +1,18 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail\Transport component.
*/
class DomainException extends Exception\DomainException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport\Exception;
use Laminas\Mail\Exception\ExceptionInterface as MailException;
interface ExceptionInterface extends MailException
{
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class InvalidArgumentException extends Exception\InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,18 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport\Exception;
use Laminas\Mail\Exception;
/**
* Exception for Laminas\Mail component.
*/
class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport;
use Laminas\Stdlib\ArrayUtils;
use Traversable;
abstract class Factory
{
/**
* @var array Known transport types
*/
protected static $classMap = [
'file' => File::class,
'inmemory' => InMemory::class,
'memory' => InMemory::class,
'null' => InMemory::class,
'sendmail' => Sendmail::class,
'smtp' => Smtp::class,
];
/**
* @param array $spec
* @return TransportInterface
* @throws Exception\InvalidArgumentException
* @throws Exception\DomainException
*/
public static function create($spec = [])
{
if ($spec instanceof Traversable) {
$spec = ArrayUtils::iteratorToArray($spec);
}
if (! is_array($spec)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects an array or Traversable argument; received "%s"',
__METHOD__,
(is_object($spec) ? get_class($spec) : gettype($spec))
));
}
$type = isset($spec['type']) ? $spec['type'] : 'sendmail';
$normalizedType = strtolower($type);
if (isset(static::$classMap[$normalizedType])) {
$type = static::$classMap[$normalizedType];
}
if (! class_exists($type)) {
throw new Exception\DomainException(sprintf(
'%s expects the "type" attribute to resolve to an existing class; received "%s"',
__METHOD__,
$type
));
}
$transport = new $type;
if (! $transport instanceof TransportInterface) {
throw new Exception\DomainException(sprintf(
'%s expects the "type" attribute to resolve to a valid %s instance; received "%s"',
__METHOD__,
TransportInterface::class,
$type
));
}
if ($transport instanceof Smtp && isset($spec['options'])) {
$transport->setOptions(new SmtpOptions($spec['options']));
}
if ($transport instanceof File && isset($spec['options'])) {
$transport->setOptions(new FileOptions($spec['options']));
}
return $transport;
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport;
use Laminas\Mail\Message;
/**
* File transport
*
* Class for saving outgoing emails in filesystem
*/
class File implements TransportInterface
{
/**
* @var FileOptions
*/
protected $options;
/**
* Last file written to
*
* @var string
*/
protected $lastFile;
/**
* Constructor
*
* @param null|FileOptions $options OPTIONAL (Default: null)
*/
public function __construct(FileOptions $options = null)
{
if (! $options instanceof FileOptions) {
$options = new FileOptions();
}
$this->setOptions($options);
}
/**
* @return FileOptions
*/
public function getOptions()
{
return $this->options;
}
/**
* Sets options
*
* @param FileOptions $options
*/
public function setOptions(FileOptions $options)
{
$this->options = $options;
}
/**
* Saves e-mail message to a file
*
* @param Message $message
* @throws Exception\RuntimeException on not writable target directory or
* on file_put_contents() failure
*/
public function send(Message $message)
{
$options = $this->options;
$filename = call_user_func($options->getCallback(), $this);
$file = $options->getPath() . DIRECTORY_SEPARATOR . $filename;
$email = $message->toString();
if (false === file_put_contents($file, $email)) {
throw new Exception\RuntimeException(sprintf(
'Unable to write mail to file (directory "%s")',
$options->getPath()
));
}
$this->lastFile = $file;
}
/**
* Get the name of the last file written to
*
* @return string
*/
public function getLastFile()
{
return $this->lastFile;
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport;
use Laminas\Mail\Exception;
use Laminas\Stdlib\AbstractOptions;
class FileOptions extends AbstractOptions
{
/**
* @var string Path to stored mail files
*/
protected $path;
/**
* @var callable
*/
protected $callback;
/**
* Set path to stored mail files
*
* @param string $path
* @throws \Laminas\Mail\Exception\InvalidArgumentException
* @return FileOptions
*/
public function setPath($path)
{
if (! is_dir($path) || ! is_writable($path)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a valid path in which to write mail files; received "%s"',
__METHOD__,
(string) $path
));
}
$this->path = $path;
return $this;
}
/**
* Get path
*
* If none is set, uses value from sys_get_temp_dir()
*
* @return string
*/
public function getPath()
{
if (null === $this->path) {
$this->setPath(sys_get_temp_dir());
}
return $this->path;
}
/**
* Set callback used to generate a file name
*
* @param callable $callback
* @throws \Laminas\Mail\Exception\InvalidArgumentException
* @return FileOptions
*/
public function setCallback($callback)
{
if (! is_callable($callback)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a valid callback; received "%s"',
__METHOD__,
(is_object($callback) ? get_class($callback) : gettype($callback))
));
}
$this->callback = $callback;
return $this;
}
/**
* Get callback used to generate a file name
*
* @return callable
*/
public function getCallback()
{
if (null === $this->callback) {
$this->setCallback(function () {
return 'LaminasMail_' . time() . '_' . mt_rand() . '.eml';
});
}
return $this->callback;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport;
use Laminas\Mail\Message;
/**
* InMemory transport
*
* This transport will just store the message in memory. It is helpful
* when unit testing, or to prevent sending email when in development or
* testing.
*/
class InMemory implements TransportInterface
{
/**
* @var null|Message
*/
protected $lastMessage;
/**
* Takes the last message and saves it for testing.
*
* @param Message $message
*/
public function send(Message $message)
{
$this->lastMessage = $message;
}
/**
* Get the last message sent.
*
* @return null|Message
*/
public function getLastMessage()
{
return $this->lastMessage;
}
}

View File

@@ -0,0 +1,336 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport;
use Laminas\Mail;
use Laminas\Mail\Address\AddressInterface;
use Laminas\Mail\Header\HeaderInterface;
use Traversable;
/**
* Class for sending email via the PHP internal mail() function
*/
class Sendmail implements TransportInterface
{
/**
* Config options for sendmail parameters
*
* @var string
*/
protected $parameters;
/**
* Callback to use when sending mail; typically, {@link mailHandler()}
*
* @var callable
*/
protected $callable;
/**
* error information
* @var string
*/
protected $errstr;
/**
* @var string
*/
protected $operatingSystem;
/**
* Constructor.
*
* @param null|string|array|Traversable $parameters OPTIONAL (Default: null)
*/
public function __construct($parameters = null)
{
if ($parameters !== null) {
$this->setParameters($parameters);
}
$this->callable = [$this, 'mailHandler'];
}
/**
* Set sendmail parameters
*
* Used to populate the additional_parameters argument to mail()
*
* @param null|string|array|Traversable $parameters
* @throws \Laminas\Mail\Transport\Exception\InvalidArgumentException
* @return Sendmail
*/
public function setParameters($parameters)
{
if ($parameters === null || is_string($parameters)) {
$this->parameters = $parameters;
return $this;
}
if (! is_array($parameters) && ! $parameters instanceof Traversable) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a string, array, or Traversable object of parameters; received "%s"',
__METHOD__,
(is_object($parameters) ? get_class($parameters) : gettype($parameters))
));
}
$string = '';
foreach ($parameters as $param) {
$string .= ' ' . $param;
}
$this->parameters = trim($string);
return $this;
}
/**
* Set callback to use for mail
*
* Primarily for testing purposes, but could be used to curry arguments.
*
* @param callable $callable
* @throws \Laminas\Mail\Transport\Exception\InvalidArgumentException
* @return Sendmail
*/
public function setCallable($callable)
{
if (! is_callable($callable)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a callable argument; received "%s"',
__METHOD__,
(is_object($callable) ? get_class($callable) : gettype($callable))
));
}
$this->callable = $callable;
return $this;
}
/**
* Send a message
*
* @param \Laminas\Mail\Message $message
*/
public function send(Mail\Message $message)
{
$to = $this->prepareRecipients($message);
$subject = $this->prepareSubject($message);
$body = $this->prepareBody($message);
$headers = $this->prepareHeaders($message);
$params = $this->prepareParameters($message);
// On *nix platforms, we need to replace \r\n with \n
// sendmail is not an SMTP server, it is a unix command - it expects LF
if (! $this->isWindowsOs()) {
$to = str_replace("\r\n", "\n", $to);
$subject = str_replace("\r\n", "\n", $subject);
$body = str_replace("\r\n", "\n", $body);
$headers = str_replace("\r\n", "\n", $headers);
}
call_user_func($this->callable, $to, $subject, $body, $headers, $params);
}
/**
* Prepare recipients list
*
* @param \Laminas\Mail\Message $message
* @throws \Laminas\Mail\Transport\Exception\RuntimeException
* @return string
*/
protected function prepareRecipients(Mail\Message $message)
{
$headers = $message->getHeaders();
$hasTo = $headers->has('to');
if (! $hasTo && ! $headers->has('cc') && ! $headers->has('bcc')) {
throw new Exception\RuntimeException(
'Invalid email; contains no at least one of "To", "Cc", and "Bcc" header'
);
}
if (! $hasTo) {
return '';
}
/** @var Mail\Header\To $to */
$to = $headers->get('to');
$list = $to->getAddressList();
if (0 == count($list)) {
throw new Exception\RuntimeException('Invalid "To" header; contains no addresses');
}
// If not on Windows, return normal string
if (! $this->isWindowsOs()) {
return $to->getFieldValue(HeaderInterface::FORMAT_ENCODED);
}
// Otherwise, return list of emails
$addresses = [];
foreach ($list as $address) {
$addresses[] = $address->getEmail();
}
$addresses = implode(', ', $addresses);
return $addresses;
}
/**
* Prepare the subject line string
*
* @param \Laminas\Mail\Message $message
* @return string
*/
protected function prepareSubject(Mail\Message $message)
{
$headers = $message->getHeaders();
if (! $headers->has('subject')) {
return;
}
$header = $headers->get('subject');
return $header->getFieldValue(HeaderInterface::FORMAT_ENCODED);
}
/**
* Prepare the body string
*
* @param \Laminas\Mail\Message $message
* @return string
*/
protected function prepareBody(Mail\Message $message)
{
if (! $this->isWindowsOs()) {
// *nix platforms can simply return the body text
return $message->getBodyText();
}
// On windows, lines beginning with a full stop need to be fixed
$text = $message->getBodyText();
$text = str_replace("\n.", "\n..", $text);
return $text;
}
/**
* Prepare the textual representation of headers
*
* @param \Laminas\Mail\Message $message
* @return string
*/
protected function prepareHeaders(Mail\Message $message)
{
// Strip the "to" and "subject" headers
$headers = clone $message->getHeaders();
$headers->removeHeader('To');
$headers->removeHeader('Subject');
/** @var Mail\Header\From $from Sanitize the From header*/
$from = $headers->get('From');
if ($from) {
foreach ($from->getAddressList() as $address) {
if (strpos($address->getEmail(), '\\"') !== false) {
throw new Exception\RuntimeException('Potential code injection in From header');
}
}
}
return $headers->toString();
}
/**
* Prepare additional_parameters argument
*
* Basically, overrides the MAIL FROM envelope with either the Sender or
* From address.
*
* @param \Laminas\Mail\Message $message
* @return string
*/
protected function prepareParameters(Mail\Message $message)
{
if ($this->isWindowsOs()) {
return;
}
$parameters = (string) $this->parameters;
if (preg_match('/(^| )\-f.+/', $parameters)) {
return $parameters;
}
$sender = $message->getSender();
if ($sender instanceof AddressInterface) {
$parameters .= ' -f' . \escapeshellarg($sender->getEmail());
return $parameters;
}
$from = $message->getFrom();
if (count($from)) {
$from->rewind();
$sender = $from->current();
$parameters .= ' -f' . \escapeshellarg($sender->getEmail());
return $parameters;
}
return $parameters;
}
/**
* Send mail using PHP native mail()
*
* @param string $to
* @param string $subject
* @param string $message
* @param string $headers
* @param $parameters
* @throws \Laminas\Mail\Transport\Exception\RuntimeException
*/
public function mailHandler($to, $subject, $message, $headers, $parameters)
{
set_error_handler([$this, 'handleMailErrors']);
if ($parameters === null) {
$result = mail($to, $subject, $message, $headers);
} else {
$result = mail($to, $subject, $message, $headers, $parameters);
}
restore_error_handler();
if ($this->errstr !== null || ! $result) {
$errstr = $this->errstr;
if (empty($errstr)) {
$errstr = 'Unknown error';
}
throw new Exception\RuntimeException('Unable to send mail: ' . $errstr);
}
}
/**
* Temporary error handler for PHP native mail().
*
* @param int $errno
* @param string $errstr
* @param string $errfile
* @param string $errline
* @param array $errcontext
* @return bool always true
*/
public function handleMailErrors($errno, $errstr, $errfile = null, $errline = null, array $errcontext = null)
{
$this->errstr = $errstr;
return true;
}
/**
* Is this a windows OS?
*
* @return bool
*/
protected function isWindowsOs()
{
if (! $this->operatingSystem) {
$this->operatingSystem = strtoupper(substr(PHP_OS, 0, 3));
}
return ($this->operatingSystem == 'WIN');
}
}

View File

@@ -0,0 +1,406 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport;
use Laminas\Mail\Address;
use Laminas\Mail\Headers;
use Laminas\Mail\Message;
use Laminas\Mail\Protocol;
use Laminas\Mail\Protocol\Exception as ProtocolException;
use Laminas\ServiceManager\ServiceManager;
/**
* SMTP connection object
*
* Loads an instance of Laminas\Mail\Protocol\Smtp and forwards smtp transactions
*/
class Smtp implements TransportInterface
{
/**
* @var SmtpOptions
*/
protected $options;
/**
* @var Envelope|null
*/
protected $envelope;
/**
* @var Protocol\Smtp
*/
protected $connection;
/**
* @var bool
*/
protected $autoDisconnect = true;
/**
* @var Protocol\SmtpPluginManager
*/
protected $plugins;
/**
* When did we connect to the server?
*
* @var int|null
*/
protected $connectedTime;
/**
* Constructor.
*
* @param SmtpOptions $options Optional
*/
public function __construct(SmtpOptions $options = null)
{
if (! $options instanceof SmtpOptions) {
$options = new SmtpOptions();
}
$this->setOptions($options);
}
/**
* Set options
*
* @param SmtpOptions $options
* @return Smtp
*/
public function setOptions(SmtpOptions $options)
{
$this->options = $options;
return $this;
}
/**
* Get options
*
* @return SmtpOptions
*/
public function getOptions()
{
return $this->options;
}
/**
* Set options
*
* @param Envelope $envelope
*/
public function setEnvelope(Envelope $envelope)
{
$this->envelope = $envelope;
}
/**
* Get envelope
*
* @return Envelope|null
*/
public function getEnvelope()
{
return $this->envelope;
}
/**
* Set plugin manager for obtaining SMTP protocol connection
*
* @param Protocol\SmtpPluginManager $plugins
* @throws Exception\InvalidArgumentException
* @return Smtp
*/
public function setPluginManager(Protocol\SmtpPluginManager $plugins)
{
$this->plugins = $plugins;
return $this;
}
/**
* Get plugin manager for loading SMTP protocol connection
*
* @return Protocol\SmtpPluginManager
*/
public function getPluginManager()
{
if (null === $this->plugins) {
$this->setPluginManager(new Protocol\SmtpPluginManager(new ServiceManager()));
}
return $this->plugins;
}
/**
* Set the automatic disconnection when destruct
*
* @param bool $flag
* @return Smtp
*/
public function setAutoDisconnect($flag)
{
$this->autoDisconnect = (bool) $flag;
return $this;
}
/**
* Get the automatic disconnection value
*
* @return bool
*/
public function getAutoDisconnect()
{
return $this->autoDisconnect;
}
/**
* Return an SMTP connection
*
* @param string $name
* @param array|null $options
* @return Protocol\Smtp
*/
public function plugin($name, array $options = null)
{
return $this->getPluginManager()->get($name, $options);
}
/**
* Class destructor to ensure all open connections are closed
*/
public function __destruct()
{
if (! $this->getConnection() instanceof Protocol\Smtp) {
return;
}
try {
$this->getConnection()->quit();
} catch (ProtocolException\ExceptionInterface $e) {
// ignore
}
if ($this->autoDisconnect) {
$this->getConnection()->disconnect();
}
}
/**
* Sets the connection protocol instance
*
* @param Protocol\AbstractProtocol $connection
*/
public function setConnection(Protocol\AbstractProtocol $connection)
{
$this->connection = $connection;
if (($connection instanceof Protocol\Smtp)
&& ($this->getOptions()->getConnectionTimeLimit() !== null)
) {
$connection->setUseCompleteQuit(false);
}
}
/**
* Gets the connection protocol instance
*
* @return Protocol\Smtp
*/
public function getConnection()
{
$timeLimit = $this->getOptions()->getConnectionTimeLimit();
if ($timeLimit !== null
&& $this->connectedTime !== null
&& ((time() - $this->connectedTime) > $timeLimit)
) {
$this->connection = null;
}
return $this->connection;
}
/**
* Disconnect the connection protocol instance
*
* @return void
*/
public function disconnect()
{
if ($this->getConnection() instanceof Protocol\Smtp) {
$this->getConnection()->disconnect();
$this->connectedTime = null;
}
}
/**
* Send an email via the SMTP connection protocol
*
* The connection via the protocol adapter is made just-in-time to allow a
* developer to add a custom adapter if required before mail is sent.
*
* @param Message $message
* @throws Exception\RuntimeException
*/
public function send(Message $message)
{
// If sending multiple messages per session use existing adapter
$connection = $this->getConnection();
if (! ($connection instanceof Protocol\Smtp) || ! $connection->hasSession()) {
$connection = $this->connect();
} else {
// Reset connection to ensure reliable transaction
$connection->rset();
}
// Prepare message
$from = $this->prepareFromAddress($message);
$recipients = $this->prepareRecipients($message);
$headers = $this->prepareHeaders($message);
$body = $this->prepareBody($message);
if ((count($recipients) == 0) && (! empty($headers) || ! empty($body))) {
// Per RFC 2821 3.3 (page 18)
throw new Exception\RuntimeException(
sprintf(
'%s transport expects at least one recipient if the message has at least one header or body',
__CLASS__
)
);
}
// Set sender email address
$connection->mail($from);
// Set recipient forward paths
foreach ($recipients as $recipient) {
$connection->rcpt($recipient);
}
// Issue DATA command to client
$connection->data($headers . Headers::EOL . $body);
}
/**
* Retrieve email address for envelope FROM
*
* @param Message $message
* @throws Exception\RuntimeException
* @return string
*/
protected function prepareFromAddress(Message $message)
{
if ($this->getEnvelope() && $this->getEnvelope()->getFrom()) {
return $this->getEnvelope()->getFrom();
}
$sender = $message->getSender();
if ($sender instanceof Address\AddressInterface) {
return $sender->getEmail();
}
$from = $message->getFrom();
if (! count($from)) {
// Per RFC 2822 3.6
throw new Exception\RuntimeException(sprintf(
'%s transport expects either a Sender or at least one From address in the Message; none provided',
__CLASS__
));
}
$from->rewind();
$sender = $from->current();
return $sender->getEmail();
}
/**
* Prepare array of email address recipients
*
* @param Message $message
* @return array
*/
protected function prepareRecipients(Message $message)
{
if ($this->getEnvelope() && $this->getEnvelope()->getTo()) {
return (array) $this->getEnvelope()->getTo();
}
$recipients = [];
foreach ($message->getTo() as $address) {
$recipients[] = $address->getEmail();
}
foreach ($message->getCc() as $address) {
$recipients[] = $address->getEmail();
}
foreach ($message->getBcc() as $address) {
$recipients[] = $address->getEmail();
}
$recipients = array_unique($recipients);
return $recipients;
}
/**
* Prepare header string from message
*
* @param Message $message
* @return string
*/
protected function prepareHeaders(Message $message)
{
$headers = clone $message->getHeaders();
$headers->removeHeader('Bcc');
return $headers->toString();
}
/**
* Prepare body string from message
*
* @param Message $message
* @return string
*/
protected function prepareBody(Message $message)
{
return $message->getBodyText();
}
/**
* Lazy load the connection
*
* @return Protocol\Smtp
*/
protected function lazyLoadConnection()
{
// Check if authentication is required and determine required class
$options = $this->getOptions();
$config = $options->getConnectionConfig();
$config['host'] = $options->getHost();
$config['port'] = $options->getPort();
$this->setConnection($this->plugin($options->getConnectionClass(), $config));
return $this->connect();
}
/**
* Connect the connection, and pass it helo
*
* @return Protocol\Smtp
*/
protected function connect()
{
if (! $this->connection instanceof Protocol\Smtp) {
return $this->lazyLoadConnection();
}
$this->connection->connect();
$this->connectedTime = time();
$this->connection->helo($this->getOptions()->getName());
return $this->connection;
}
}

View File

@@ -0,0 +1,209 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport;
use Laminas\Mail\Exception;
use Laminas\Stdlib\AbstractOptions;
class SmtpOptions extends AbstractOptions
{
/**
* @var string Local client hostname
*/
protected $name = 'localhost';
/**
* @var string
*/
protected $connectionClass = 'smtp';
/**
* Connection configuration (passed to the underlying Protocol class)
*
* @var array
*/
protected $connectionConfig = [];
/**
* @var string Remote SMTP hostname or IP
*/
protected $host = '127.0.0.1';
/**
* @var int
*/
protected $port = 25;
/**
* The timeout in seconds for the SMTP connection
* (Use null to disable it)
*
* @var int|null
*/
protected $connectionTimeLimit;
/**
* Return the local client hostname
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set the local client hostname or IP
*
* @todo hostname/IP validation
* @param string $name
* @throws \Laminas\Mail\Exception\InvalidArgumentException
* @return SmtpOptions
*/
public function setName($name)
{
if (! is_string($name) && $name !== null) {
throw new Exception\InvalidArgumentException(sprintf(
'Name must be a string or null; argument of type "%s" provided',
(is_object($name) ? get_class($name) : gettype($name))
));
}
$this->name = $name;
return $this;
}
/**
* Get connection class
*
* This should be either the class Laminas\Mail\Protocol\Smtp or a class
* extending it -- typically a class in the Laminas\Mail\Protocol\Smtp\Auth
* namespace.
*
* @return string
*/
public function getConnectionClass()
{
return $this->connectionClass;
}
/**
* Set connection class
*
* @param string $connectionClass the value to be set
* @throws \Laminas\Mail\Exception\InvalidArgumentException
* @return SmtpOptions
*/
public function setConnectionClass($connectionClass)
{
if (! is_string($connectionClass) && $connectionClass !== null) {
throw new Exception\InvalidArgumentException(sprintf(
'Connection class must be a string or null; argument of type "%s" provided',
(is_object($connectionClass) ? get_class($connectionClass) : gettype($connectionClass))
));
}
$this->connectionClass = $connectionClass;
return $this;
}
/**
* Get connection configuration array
*
* @return array
*/
public function getConnectionConfig()
{
return $this->connectionConfig;
}
/**
* Set connection configuration array
*
* @param array $connectionConfig
* @return SmtpOptions
*/
public function setConnectionConfig(array $connectionConfig)
{
$this->connectionConfig = $connectionConfig;
return $this;
}
/**
* Get the host name
*
* @return string
*/
public function getHost()
{
return $this->host;
}
/**
* Set the SMTP host
*
* @todo hostname/IP validation
* @param string $host
* @return SmtpOptions
*/
public function setHost($host)
{
$this->host = (string) $host;
return $this;
}
/**
* Get the port the SMTP server runs on
*
* @return int
*/
public function getPort()
{
return $this->port;
}
/**
* Set the port the SMTP server runs on
*
* @param int $port
* @throws \Laminas\Mail\Exception\InvalidArgumentException
* @return SmtpOptions
*/
public function setPort($port)
{
$port = (int) $port;
if ($port < 1) {
throw new Exception\InvalidArgumentException(sprintf(
'Port must be greater than 1; received "%d"',
$port
));
}
$this->port = $port;
return $this;
}
/**
* @return int|null
*/
public function getConnectionTimeLimit()
{
return $this->connectionTimeLimit;
}
/**
* @param int|null $seconds
* @return self
*/
public function setConnectionTimeLimit($seconds)
{
$this->connectionTimeLimit = $seconds === null
? null
: (int) $seconds;
return $this;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @see https://github.com/laminas/laminas-mail for the canonical source repository
* @copyright https://github.com/laminas/laminas-mail/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-mail/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Mail\Transport;
use Laminas\Mail;
/**
* Interface for mail transports
*/
interface TransportInterface
{
/**
* Send a mail message
*
* @param \Laminas\Mail\Message $message
* @return
*/
public function send(Mail\Message $message);
}