diff --git a/Message/Mail/DsnNotificationType.php b/Message/Mail/DsnNotificationType.php new file mode 100644 index 000000000..f7864ff90 --- /dev/null +++ b/Message/Mail/DsnNotificationType.php @@ -0,0 +1,38 @@ +. If empty this is automatically generated. + * + * @var string + * @since 1.0.0 + */ + protected string $messageId = ''; + + /** + * Unique ID used for message ID and boundaries. + * + * @var string + * @since 1.0.0 + */ + protected string $uniqueid = ''; + + /** + * Mailer for sending message + * + * @var string + * @since 1.0.0 + */ + protected string $mailer = SubmitType::MAIL; + + /** + * Mail from. + * + * @var string + * @since 1.0.0 + */ + protected string $from = ''; + + /** + * Mail from. + * + * @var string + * @since 1.0.0 + */ + protected string $fromName = ''; + + /** + * Return path/bounce address + * + * @var string + * @since 1.0.0 + */ + public string $sender = ''; + + /** + * Confirm address. + * + * @var string + */ + public string $confirmAddress = ''; + + /** + * Mail to. + * + * @var array + * @since 1.0.0 + */ + public array $to = []; + + /** + * Mail subject. + * + * @var string + * @since 1.0.0 + */ + public string $subject = ''; + + /** + * Mail cc. + * + * @var array + * @since 1.0.0 + */ + public array $cc = []; + + /** + * Mail bcc. + * + * @var array + * @since 1.0.0 + */ + public array $bcc = []; + + /** + * The array of reply-to names and addresses. + * + * @var array + * @since 1.0.0 + */ + public array $replyTo = []; + + /** + * Mail attachments. + * + * @var array + * @since 1.0.0 + */ + protected array $attachment = []; + + /** + * Mail body. + * + * @var string + * @since 1.0.0 + */ + public string $body = ''; + + /** + * Mail alt. + * + * @var string + * @since 1.0.0 + */ + public string $bodyAlt = ''; + + /** + * Mail mime. + * + * @var string + * @since 1.0.0 + */ + public string $bodyMime = ''; + + /** + * The array of MIME boundary strings. + * + * @var array + * @since 1.0.0 + */ + protected array $boundary = []; + + /** + * Mail header. + * + * @var string + * @since 1.0.0 + */ + protected string $header = ''; + + /** + * Mail header. + * + * @var string + * @since 1.0.0 + */ + public string $headerMime = ''; + + /** + * The array of custom headers. + * + * @var array + * @since 1.0.0 + */ + protected array $customHeader = []; + + /** + * Word wrap. + * + * @var int + * @since 1.0.0 + */ + protected int $wordWrap = 72; + + /** + * Encoding. + * + * @var string + * @since 1.0.0 + */ + protected string $encoding = EncodingType::E_8BIT; + + /** + * Mail content type. + * + * @var string + * @since 1.0.0 + */ + protected string $contentType = MimeType::M_TXT; + + /** + * Character set + * + * @var string + * @since 1.0.0 + */ + protected string $charset = CharsetType::ISO_8859_1; + + /** + * Mail message type. + * + * @var string + * @since 1.0.0 + */ + protected string $messageType = ''; + + /** + * Mail from. + * + * @var null|\DateTime + * @since 1.0.0 + */ + public ?\DateTimeImmutable $messageDate = null; + + /** + * Priority + * + * @var int + * @since 1.0.0 + */ + public int $priority = 0; + + /** + * Should confirm reading + * + * @var bool + * @since 1.0.0 + */ + protected bool $confirmReading = false; + + /** + * The S/MIME certificate file path. + * + * @var string + * @since 1.0.0 + */ + protected string $signCertFile = ''; + + /** + * The S/MIME key file path. + * + * @var string + * @since 1.0.0 + */ + protected string $signKeyFile = ''; + + /** + * The optional S/MIME extra certificates ("CA Chain") file path. + * + * @var string + * @since 1.0.0 + */ + protected string $signExtracertFiles = ''; + + /** + * The S/MIME password for the key. + * Used only if the key is encrypted. + * + * @var string + * @since 1.0.0 + */ + protected string $signKeyPass = ''; + + /** + * DKIM selector. + * + * @var string + * @since 1.0.0 + */ + public string $dkimSelector = ''; + + /** + * DKIM Identity. + * Usually the email address used as the source of the email. + * + * @var string + * @since 1.0.0 + */ + public string $dkimIdentity = ''; + + /** + * DKIM passphrase. + * Used if your key is encrypted. + * + * @var string + * @since 1.0.0 + */ + public string $dkimPass = ''; + + /** + * DKIM signing domain name. + * + * @var string + * @since 1.0.0 + */ + public string $dkimDomain = ''; + + /** + * DKIM Copy header field values for diagnostic use. + * + * @var bool + * @since 1.0.0 + */ + public bool $dkimCopyHeader = true; + + /** + * DKIM Extra signing headers. + * + * @example ['List-Unsubscribe', 'List-Help'] + * + * @var array + * @since 1.0.0 + */ + public array $dkimHeaders = []; + + /** + * DKIM private key file path. + * + * @var string + * @since 1.0.0 + */ + public string $dkimPrivatePath = ''; + + /** + * DKIM private key string. + * + * If set, takes precedence over `$dkimPrivatePath`. + * + * @var string + * @since 1.0.0 + */ + public string $dkimPrivateKey = ''; + + /** + * Constructor. + * + * @param string $id Id + * + * @since 1.0.0 + */ + public function __construct(string $id = '') + { + $this->id = $id; + } + + /** + * Set the From and FromName. + * + * @param string $address Email address + * @param string $name Name + * + * @return bool + * + * @since 1.0.0 + */ + public function setFrom(string $address, string $name = '') : bool + { + $address = \trim($address); + $name = \trim(\preg_replace('/[\r\n]+/', '', $name)); + + if (!EmailValidator::isValid($address)) { + return false; + } + + $this->from = $address; + $this->fromName = $name; + + if (empty($this->sender)) { + $this->sender = $address; + } + + return true; + } + + /** + * Sets message type to html or plain. + * + * @param bool $isHtml Html mode + * + * @return void + * + * @since 1.0.0 + */ + public function setHtml(bool $isHtml = true) : void + { + $this->contentType = $isHtml ? MimeType::M_HTML : MimeType::M_TEXT; + } + + /** + * Add a "To" address. + * + * @param string $address Email address + * @param string $name Name + * + * @return bool + * + * @since 1.0.0 + */ + public function addTo(string $address, string $name = '') : bool + { + if (!EmailValidator::isValid($address)) { + return false; + } + + $this->to[$address] = [$address, $name]; + + return true; + } + + /** + * Add a "CC" address. + * + * @param string $address Email address + * @param string $name Name + * + * @return bool + * + * @since 1.0.0 + */ + public function addCC(string $address, string $name = '') : bool + { + if (!EmailValidator::isValid($address)) { + return false; + } + + $this->cc[$address] = [$address, $name]; + + return true; + } + + /** + * Add a "BCC" address. + * + * @param string $address Email address + * @param string $name Name + * + * @return bool + * + * @since 1.0.0 + */ + public function addBCC(string $address, string $name = '') : bool + { + if (!EmailValidator::isValid($address)) { + return false; + } + + $this->bcc[$address] = [$address, $name]; + + return true; + } + + /** + * Add a "Reply-To" address. + * + * @param string $address Email address + * @param string $name Name + * + * @return bool + * + * @since 1.0.0 + */ + public function addReplyTo(string $address, string $name = '') : bool + { + if (!EmailValidator::isValid($address)) { + return false; + } + + $this->replyTo[$address] = [$address, $name]; + + return true; + } + + /** + * Parse and validate a string containing one or more RFC822-style comma-separated email addresses + * of the form "display name
" into an array of name/address pairs. + * + * @param string $addrstr Address line + * @param bool $useImap Use imap for parsing + * + * @return array + * + * @since 1.0.0 + */ + public static function parseAddresses(string $addrstr, bool $useimap = true) : array + { + $addresses = []; + if ($useimap && \function_exists('imap_rfc822_parse_adrlist')) { + $list = \imap_rfc822_parse_adrlist($addrstr, ''); + foreach ($list as $address) { + if (('.SYNTAX-ERROR.' !== $address->host) + && EmailValidator::isValid($address->mailbox . '@' . $address->host) + ) { + $addresses[] = [ + 'name' => (\property_exists($address, 'personal') ? $address->personal : ''), + 'address' => $address->mailbox . '@' . $address->host, + ]; + } + } + + return $addresses; + } + + $list = \explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + if (\strpos($address, '<') === false) { + if (EmailValidator::isValid($address)) { + $addresses[] = [ + 'name' => '', + 'address' => $address, + ]; + } + } else { + list($name, $email) = \explode('<', $address); + $email = \trim(\str_replace('>', '', $email)); + + if (EmailValidator::isValid($email)) { + $addresses[] = [ + 'name' => \trim(\str_replace(['"', "'"], '', $name)), + 'address' => $email, + ]; + } + } + } + + return $addresses; + } + + /** + * Pre-send preparations + * + * @param string $mailer Mailer tool + * + * @return bool + * + * @since 1.0.0 + */ + public function preSend(string $mailer) : bool + { + $this->header = ''; + $this->mailer = $mailer; + + if (\count($this->to) + \count($this->cc) + \count($this->bcc) < 1) { + return false; + } + + if (!empty($this->altBody)) { + $this->contentType = MimeType::M_ALT; + } + + $this->setMessageType(); + + $this->headerMime = ''; + $this->bodyMime = $this->createBody(); + + $tempheaders = $this->headerMime; + $this->headerMime = $this->createHeader(); + $this->headerMime .= $tempheaders; + + if ($this->mailer === SubmitType::MAIL) { + $this->header .= \count($this->to) > 0 + ? $this->createAddressList('To', $this->to) + : 'Subject: undisclosed-recipients:;' . self::$LE; + + $this->header .= 'Subject: ' . $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $this->subject))) . self::$LE; + } + + // Sign with DKIM if enabled + if (!empty($this->dkimDomain) + && !empty($this->dkimSelector) + && (!empty($this->dkimPrivateKey) + || (!empty($this->dkimPrivatePath) + && FileUtils::isPermittedPath($this->dkimPrivatePath) + && \is_file($this->dkimPrivatePath) + ) + ) + ) { + $headerDkim = $this->dkimAdd( + $this->headerMime . $this->header, + $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $this->subject))), + $this->bodyMime + ); + + $this->headerMime = rtrim($this->headerMime, " \r\n\t") . self::$LE . + self::normalizeBreaks($headerDkim, self::$LE) . self::$LE; + } + + return true; + } + + /** + * Assemble message headers. + * + * @return string The assembled headers + * + * @since 1.0.0 + */ + private function createHeader() : string + { + $result = ''; + $result .= 'Date : ' . ($this->messageDate === null + ? (new \DateTime('now'))->format('D, j M Y H:i:s O') + : $this->messageDate->format('D, j M Y H:i:s O')) + . self::$LE; + + if ($this->mailer === SubmitType::MAIL) { + $result .= \count($this->to) > 0 + ? $this->addrAppend('To', $this->to) + : 'To: undisclosed-recipients:;' . self::$LE; + } + + $result .= $this->addrAppend('From', [[\trim($this->from), $this->fromName]]); + + // sendmail and mail() extract Cc from the header before sending + if (\count($this->cc) > 0) { + $result .= $this->addrAppend('Cc', $this->cc); + } + + // sendmail and mail() extract Bcc from the header before sending + if (($this->mailer === SubmitType::MAIL || $this->mailer === SubmitType::SENDMAIL || $this->mailer === SubmitType::QMAIL) + && \count($this->bcc) > 0 + ) { + $result .= $this->addrAppend('Bcc', $this->bcc); + } + + if (\count($this->replyTo) > 0) { + $result .= $this->addrAppend('Reply-To', $this->replyTo); + } + + // mail() sets the subject itself + if ($this->mailer !== SubmitType::MAIL) { + $result .= 'Subject: ' . $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $this->subject))) . self::$LE; + } + + $this->hostname = empty($this->hostname) ? SystemUtils::getHostname() : $this->hostname; + + // Only allow a custom message Id if it conforms to RFC 5322 section 3.6.4 + // https://tools.ietf.org/html/rfc5322#section-3.6.4 + $this->messageId = $this->messageId !== '' && \preg_match('/^<.*@.*>$/', $this->messageId) + ? $this->messageId + : \sprintf('<%s@%s>', $this->uniqueid, $this->hostname); + + $result .= 'Message-ID: ' . $this->messageId . self::$LE; + + if ($this->priority > 0) { + $result .= 'X-Priority: ' . $this->priority . self::$LE; + } + + $result .= 'X-Mailer: ' . self::XMAILER . self::$LE; + + if ($this->confirmAddress !== '') { + $result .= 'Disposition-Notification-To: ' . '<' . $this->confirmAddress . '>' . self::$LE; + } + + // Add custom headers + foreach ($this->customHeader as $header) { + $result .= \trim($header[0]) . ': ' . $this->encodeHeader(\trim($header[1])) . self::$LE; + } + + if (!empty($this->signKeyFile)) { + $result .= 'MIME-Version: 1.0' . self::$LE; + $result .= $this->getMailMime(); + } + + return $result; + } + + /** + * Create recipient headers. + * + * @param string $type Address type + * @param array $addr Address 0 = address, 1 = name ['joe@example.com', 'Joe User'] + * + * @return string + * + * @since 1.0.0 + */ + private function addrAppend(string $type, array $addr) : string + { + $addresses = []; + foreach ($addr as $address) { + $addresses[] = $this->addrFormat($address); + } + + return $type . ': ' . \implode(', ', $addresses) . self::$LE; + } + + /** + * Format an address for use in a message header. + * + * @param array $addr Address 0 = address, 1 = name ['joe@example.com', 'Joe User'] + * + * @return string + * + * @since 1.0.0 + */ + public function addrFormat(array $addr) : string + { + if (empty($addr[1])) { + return \trim(\str_replace(["\r", "\n"], '', $addr[0])); + } + + return $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $addr[1])), 'phrase') . + ' <' . \trim(\str_replace(["\r", "\n"], '', $addr[0])) . '>'; + } + + /** + * Get the message MIME type headers. + * + * @return string + * + * @since 1.0.0 + */ + private function getMailMime() : string + { + $result = ''; + $isMultipart = true; + + switch ($this->messageType) { + case 'inline': + $result .= 'Content-Type: ' . MimeType::M_RELATED . ';' . self::$LE; + $result .= ' boundary="' . $this->boundary[1] . '"' . self::$LE; + break; + case 'attach': + case 'inline_attach': + case 'alt_attach': + case 'alt_inline_attach': + $result .= 'Content-Type: ' . MimeType::M_MIXED . ';' . self::$LE; + $result .= ' boundary="' . $this->boundary[1] . '"' . self::$LE; + break; + case 'alt': + case 'alt_inline': + $result .= 'Content-Type: ' . MimeType::M_ALT . ';' . self::$LE; + $result .= ' boundary="' . $this->boundary[1] . '"' . self::$LE; + break; + default: + // Catches case 'plain': and case '': + $result .= 'Content-Type: ' . $this->contentType . '; charset=' . $this->charset . self::$LE; + $isMultipart = false; + break; + } + + // RFC1341 part 5 says 7bit is assumed if not specified + if ($this->encoding === EncodingType::E_7BIT) { + return $result; + } + + // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE + if ($isMultipart) { + if ($this->encoding === EncodingType::E_8BIT) { + $result .= 'Content-Transfer-Encoding: ' . EncodingType::E_8BIT . self::$LE; + } + + // quoted-printable and base64 are 7bit compatible + } else { + $result .= 'Content-Transfer-Encoding: ' . $this->encoding . self::$LE; + } + + return $result; + } + + /** + * Converts IDN in given email address to its ASCII form + * + * @param string $charset Charset + * @param string $address Email address + * + * @return string The encoded address in ASCII form + * + * @since 1.0.0 + */ + private function punyencodeAddress(string $charset, string $address) : string + { + if (empty($charset) || !EmailValidator::isValid($address)) { + return $address; + } + + $pos = \strrpos($address, '@'); + $domain = \substr($address, ++$pos); + + if (!((bool) \preg_match('/[\x80-\xFF]/', $domain)) || !\mb_check_encoding($domain, $charset)) { + return $address; + } + + $domain = \mb_convert_encoding($domain, 'UTF-8', $charset); + + $errorcode = 0; + if (defined('INTL_IDNA_VARIANT_UTS46')) { + $punycode = \idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_UTS46); + } else { + $punycode = \idn_to_ascii($domain, $errorcode); + } + + if ($punycode !== false) { + return \substr($address, 0, $pos) . $punycode; + } + + return $address; + } + + /** + * Create a unique ID to use for boundaries. + * + * @return string + * + * @since 1.0.0 + */ + protected function generateId() : string + { + $len = 32; //32 bytes = 256 bits + $bytes = ''; + $bytes = \random_bytes($len); + + if ($bytes === '') { + $bytes = \hash('sha256', \uniqid((string) \mt_rand(), true), true); + } + + return \str_replace(['=', '+', '/'], '', \base64_encode(\hash('sha256', $bytes, true))); + } + + /** + * Assemble the message body. + * + * @return string Empty on failure + * + * @since 1.0.0 + */ + public function createBody() : string + { + $body = ''; + $this->uniqueid = $this->generateId(); + + $this->boundary[1] = 'b1_' . $this->uniqueid; + $this->boundary[2] = 'b2_' . $this->uniqueid; + $this->boundary[3] = 'b3_' . $this->uniqueid; + + if (!empty($this->signKeyFile)) { + $body .= $this->getMailMime() . self::$LE; + } + + $this->setWordWrap(); + + $bodyEncoding = $this->encoding; + $bodyCharSet = $this->charset; + + // Can we do a 7-bit downgrade? + if ($bodyEncoding === EncodingType::E_8BIT && !((bool) \preg_match('/[\x80-\xFF]/', $this->body))) { + $bodyEncoding = EncodingType::E_7BIT; + + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $bodyCharSet = CharsetType::ASCII; + } + + // If lines are too long, and we're not already using an encoding that will shorten them, + // change to quoted-printable transfer encoding for the body part only + if ($this->encoding !== EncodingType::E_BASE64 + && ((bool) \preg_match('/^(.{' . (self::MAX_LINE_LENGTH + \strlen(self::$LE)) . ',})/m', $this->body)) + ) { + $bodyEncoding = EncodingType::E_QUOTED; + } + + $altBodyEncoding = $this->encoding; + $altBodyCharSet = $this->charset; + + //Can we do a 7-bit downgrade? + if ($altBodyEncoding === EncodingType::E_8BIT && !((bool) \preg_match('/[\x80-\xFF]/', $this->bodyAlt))) { + $altBodyEncoding = EncodingType::E_7BIT; + + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $altBodyCharSet = CharsetType::ASCII; + } + + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the alt body part only + if ($altBodyEncoding !== EncodingType::E_BASE64 + && ((bool) \preg_match('/^(.{' . (self::MAX_LINE_LENGTH + \strlen(self::$LE)) . ',})/m', $this->bodyAlt)) + ) { + $altBodyEncoding = EncodingType::E_QUOTED; + } + + //Use this as a preamble in all multipart message types + $mimePre = 'This is a multi-part message in MIME format.' . self::$LE . self::$LE; + switch ($this->messageType) { + case 'inline': + $body .= $mimePre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; + $body .= $this->attachAll('inline', $this->boundary[1]); + break; + case 'attach': + $body .= $mimePre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'inline_attach': + $body .= $mimePre; + $body .= '--' . $this->boundary[1] . self::$LE; + $body .= 'Content-Type: ' . MimeType::M_RELATED . ';' . self::$LE; + $body .= ' boundary="' . $this->boundary[2] . '";' . self::$LE; + $body .= ' type="' . MimeType::M_HTML . '"' . self::$LE . self::$LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; + $body .= $this->attachAll('inline', $this->boundary[2]) . self::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'alt': + $body .= $mimePre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, MimeType::M_TEXT, $altBodyEncoding); + $body .= $this->encodeString($this->bodyAlt, $altBodyEncoding) . self::$LE; + $body .= $this->getBoundary( $this->boundary[1], $bodyCharSet, MimeType::M_HTML, $bodyEncoding); + $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; + + if (!empty($this->ical)) { + $method = ICALMethodType::REQUEST; + $methods = ICALMethodType::getConstants(); + + foreach ($methods as $imethod) { + if (\stripos($this->ical, 'METHOD:' . $imethod) !== false) { + $method = $imethod; + break; + } + } + + $body .= $this->getBoundary($this->boundary[1], '', MimeType::M_ICS . '; method=' . $method, ''); + $body .= $this->encodeString($this->ical, $this->encoding) . self::$LE; + } + + $body .= self::$LE . '--' . $this->boundary[1] . '--' . self::$LE; + break; + case 'alt_inline': + $body .= $mimePre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, MimeType::M_TEXT, $altBodyEncoding); + $body .= $this->encodeString($this->bodyAlt, $altBodyEncoding) . self::$LE; + $body .= '--' . $this->boundary[1] . self::$LE; + $body .= 'Content-Type: ' . MimeType::M_RELATED . ';' . self::$LE; + $body .= ' boundary="' . $this->boundary[2] . '";' . self::$LE; + $body .= ' type="' . MimeType::M_HTML . '"' . self::$LE . self::$LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, MimeType::M_HTML, $bodyEncoding); + $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; + $body .= $this->attachAll('inline', $this->boundary[2]) . self::$LE; + $body .= self::$LE . '--' . $this->boundary[1] . '--' . self::$LE; + break; + case 'alt_attach': + $body .= $mimePre; + $body .= '--' . $this->boundary[1] . self::$LE; + $body .= 'Content-Type: ' . MimeType::M_ALT . ';' . self::$LE; + $body .= ' boundary="' . $this->boundary[2] . '"' . self::$LE . self::$LE; + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, MimeType::M_TEXT, $altBodyEncoding); + $body .= $this->encodeString($this->bodyAlt, $altBodyEncoding) . self::$LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, MimeType::M_HTML, $bodyEncoding); + $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; + + if (!empty($this->ical)) { + $method = ICALMethodType::REQUEST; + $methods = ICALMethodType::getConstants(); + + foreach ($methods as $imethod) { + if (\stripos($this->ical, 'METHOD:' . $imethod) !== false) { + $method = $imethod; + break; + } + } + + $body .= $this->getBoundary($this->boundary[2], '', MimeType::M_ICS . '; method=' . $method, ''); + $body .= $this->encodeString($this->ical, $this->encoding); + } + + $body .= self::$LE . '--' . $this->boundary[2] . '--' . self::$LE . self::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'alt_inline_attach': + $body .= $mimePre; + $body .= '--' . $this->boundary[1] . self::$LE; + $body .= 'Content-Type: ' . MimeType::M_ALT . ';' . self::$LE; + $body .= ' boundary="' . $this->boundary[2] . '"' . self::$LE; + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, MimeType::M_TEXT, $altBodyEncoding); + $body .= $this->encodeString($this->bodyAlt, $altBodyEncoding) . self::$LE; + $body .= '--' . $this->boundary[2] . self::$LE; + $body .= 'Content-Type: ' . MimeType::M_RELATED . ';' . self::$LE; + $body .= ' boundary="' . $this->boundary[3] . '";' . self::$LE; + $body .= ' type="' . MimeType::M_HTML . '"' . self::$LE . self::$LE; + $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, MimeType::M_HTML, $bodyEncoding); + $body .= $this->encodeString($this->body, $bodyEncoding) . self::$LE; + $body .= $this->attachAll('inline', $this->boundary[3]) . self::$LE; + $body .= self::$LE . '--' . $this->boundary[2] . '--' . self::$LE . self::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + default: + // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types + $this->encoding = $bodyEncoding; + $body .= $this->encodeString($this->body, $this->encoding); + break; + } + + if (!empty($this->signKeyFile)) { + if (!defined('PKCS7_TEXT')) { + return ''; + } + + $file = \tempnam(\sys_get_temp_dir(), 'srcsign'); + $signed = \tempnam(\sys_get_temp_dir(), 'mailsign'); + \file_put_contents($file, $body); + + //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197 + $sign = empty($this->signExtracertFiles) + ? \openssl_pkcs7_sign($file, $signed, + 'file://' . \realpath($this->signCertFile), + ['file://' . \realpath($this->signKeyFile), $this->signKeyPass], + [] + ) + : \openssl_pkcs7_sign($file, $signed, + 'file://' . \realpath($this->signCertFile), + ['file://' . \realpath($this->signKeyFile), $this->signKeyPass], + [], + PKCS7_DETACHED, + $this->signExtracertFiles + ); + + \unlink($file); + if ($sign === false) { + \unlink($signed); + return ''; + } + + $body = \file_get_contents($signed); + \unlink($signed); + + //The message returned by openssl contains both headers and body, so need to split them up + $parts = \explode("\n\n", $body, 2); + $this->headerMime .= $parts[0] . self::$LE . self::$LE; + $body = $parts[1]; + } + + return $body; + } + + /** + * Return the start of a message boundary. + * + * @param string $boundary Boundary + * @param string $charset Charset + * @param string $contentType Content type + * @param string $encoding Concoding + * + * @return string + * + * @since 1.0.0 + */ + protected function getBoundary(string $boundary, string $charset, string $contentType, string $encoding) : string + { + $result = ''; + if ($charset === '') { + $charset = $this->charset; + } + + if ($contentType === '') { + $contentType = $this->contentType; + } + + if ($encoding === '') { + $encoding = $this->encoding; + } + + $result .= '--' . $boundary . self::$LE; + $result .= \sprintf('Content-Type: %s; charset=%s', $contentType, $charset); + $result .= self::$LE; + + // RFC1341 part 5 says 7bit is assumed if not specified + if ($encoding !== EncodingType::E_7BIT) { + $result .= 'Content-Transfer-Encoding: ' . $encoding . self::$LE; + } + + return $result . self::$LE; + } + + /** + * Attach all file, string, and binary attachments to the message. + * + * @param string $dispositionType Disposition type + * @param string $boundary Boundary string + * + * @return string + * + * @since 1.0.0 + */ + protected function attachAll(string $dispositionType, string $boundary) : string + { + $mime = []; + $cidUniq = []; + $incl = []; + + $attachments = $this->getAttachments(); + foreach ($attachments as $attachment) { + if ($attachment[6] !== $dispositionType) { + continue; + } + + $bString = $attachment[5]; + $string = $bString ? $attachment[0] : ''; + $path = !$bString ? $attachment[0] : ''; + + $inclHash = \hash('sha256', \serialize($attachment)); + if (\in_array($inclHash, $incl, true)) { + continue; + } + + $incl[] = $inclHash; + $name = $attachment[2]; + $encoding = $attachment[3]; + $type = $attachment[4]; + $disposition = $attachment[6]; + $cid = $attachment[7]; + + if ($disposition === 'inline' && isset($cidUniq[$cid])) { + continue; + } + + $cidUniq[$cid] = true; + $mime[] = \sprintf('--%s%s', $boundary, self::$LE); + + //Only include a filename property if we have one + $mime[] = !empty($name) + ? \sprintf('Content-Type: %s; name=%s%s', + $type, + self::quotedString($this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $name)))), + self::$LE + ) + : \sprintf('Content-Type: %s%s', + $type, + self::$LE + ); + + // RFC1341 part 5 says 7bit is assumed if not specified + if ($encoding !== EncodingType::E_7BIT) { + $mime[] = \sprintf('Content-Transfer-Encoding: %s%s', $encoding, self::$LE); + } + + //Only set Content-IDs on inline attachments + if ((string) $cid !== '' && $disposition === 'inline') { + $mime[] = 'Content-ID: <' . $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $cid))) . '>' . self::$LE; + } + + // Allow for bypassing the Content-Disposition header + if (!empty($disposition)) { + $encodedName = $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $name))); + $mime[] = !empty($encodedName) + ? \sprintf('Content-Disposition: %s; filename=%s%s', + $disposition, + self::quotedString($encodedName), + self::$LE . self::$LE + ) + : \sprintf('Content-Disposition: %s%s', $disposition, self::$LE . self::$LE); + } else { + $mime[] = self::$LE; + } + + // Encode as string attachment + $mime[] = $bString + ? $this->encodeString($string, $encoding) + : $this->encodeFile($path, $encoding); + + $mime[] = self::$LE; + } + + $mime[] = \sprintf('--%s--%s', $boundary, self::$LE); + + return \implode('', $mime); + } + + /** + * If a string contains any "special" characters, double-quote the name, + * and escape any double quotes with a backslash. + * + * @param string $str String to quote + * + * @return string + * + * @since 1.0.0 + */ + private static function quotedString(string $str) : string + { + if (\preg_match('/[ ()<>@,;:"\/\[\]?=]/', $str)) { + return '"' . \str_replace('"', '\\"', $str) . '"'; + } + + return $str; + } + + /** + * Encode a file attachment in requested format. + * + * @param string $path Path + * @param string $encoding Encoding + * + * @return string + * + * @since 1.0.0 + */ + private function encodeFile(string $path, string $encoding = EncodingType::E_BASE64) : string + { + if (!FileUtils::isAccessible($path)) { + return ''; + } + + $fileBuffer = file_get_contents($path); + if (false === $fileBuffer) { + return ''; + } + + $fileBuffer = $this->encodeString($fileBuffer, $encoding); + + return $fileBuffer; + } + + /** + * Encode a string in requested format. + * + * @param string $str The text to encode + * @param string $encoding Encoding + * + * @return string + * + * @since 1.0.0 + */ + private function encodeString(string $str, string $encoding = EncodingType::E_BASE64) : string + { + $encoded = ''; + switch (strtolower($encoding)) { + case EncodingType::E_BASE64: + $encoded = \chunk_split(\base64_encode($str), self::STD_LINE_LENGTH, self::$LE); + break; + case EncodingType::E_7BIT: + case EncodingType::E_8BIT: + $encoded = self::normalizeBreaks($str, self::$LE); + if (\substr($encoded, -(\strlen(self::$LE))) !== self::$LE) { + $encoded .= self::$LE; + } + + break; + case EncodingType::E_BINARY: + $encoded = $str; + break; + case EncodingType::E_QUOTED: + $encoded = self::normalizeBreaks(\quoted_printable_encode($str), self::$LE); + break; + default: + return ''; + } + + return $encoded; + } + + /** + * Set message type based on content + * + * @return void + * + * @since 1.0.0 + */ + protected function setMessageType() : void + { + $type = []; + if (!empty($this->altBody)) { + $type[] = 'alt'; + } + + if ($this->hasInlineImage()) { + $type[] = 'inline'; + } + + if ($this->hasAttachment()) { + $type[] = 'attach'; + } + + $this->messageType = \implode('_', $type); + if ($this->messageType === '') { + $this->messageType = 'plain'; + } + } + + /** + * Mail has inline image + * + * @return bool + * + * @since 1.0.0 + */ + public function hasInlineImage() : bool + { + foreach ($this->attachment as $attachment) { + if ($attachment[6] === 'inline') { + return true; + } + } + + return false; + } + + /** + * Mail has attachment + * + * @return bool + * + * @since 1.0.0 + */ + public function hasAttachment() : bool + { + foreach ($this->attachment as $attachment) { + if ($attachment[6] === 'attachment') { + return true; + } + } + + return false; + } + + + /** + * Create address list + * + * @param string $type Address type + * @param array $addr Addresses + * + * @return string + * + * @since 1.0.0 + */ + public function createAddressList(string $type, array $addr) : string + { + $addresses = []; + foreach ($addr as $address) { + $addresses[] = $this->addrFormat($address); + } + + return $type . ': ' . implode(', ', $addresses) . static::$LE; + } + + /** + * Apply word wrapping + * + * @return void + * + * @return 1.0.0 + */ + public function setWordWrap() : void + { + if ($this->wordWrap < 1) { + return; + } + + switch ($this->messageType) { + case 'alt': + case 'alt_inline': + case 'alt_attach': + case 'alt_inline_attach': + $this->altBody = $this->wrapText($this->altBody, $this->wordWrap); + break; + default: + $this->body = $this->wrapText($this->body, $this->wordWrap); + break; + } + } + + /** + * Word-wrap message. + * Original written by philippe. + * + * @param string $message The message to wrap + * @param int $length The line length to wrap to + * @param bool $qpMode Use Quoted-Printable mode + * + * @return string + * + * @since 1.0.0 + */ + private function wrapText(string $message, int $length, bool $qpMode = false) : string + { + $softBreak = $qpMode ? \sprintf(' =%s', self::$LE) : self::$LE; + + // Don't split multibyte characters + $isUtf8 = \strtolower($this->charset) === CharsetType::UTF_8; + $leLen = \strlen(self::$LE); + $crlfLen = \strlen(self::$LE); + + $message = self::normalizeBreaks($message, self::$LE); + + //Remove a trailing line break + if (\substr($message, -$leLen) === self::$LE) { + $message = \substr($message, 0, -$leLen); + } + + //Split message into lines + $lines = \explode(self::$LE, $message); + + $message = ''; + foreach ($lines as $line) { + $words = \explode(' ', $line); + $buf = ''; + $firstword = true; + + foreach ($words as $word) { + if ($qpMode && \strlen($word) > $length) { + $spaceLeft = $length - \strlen($buf) - $crlfLen; + + if (!$firstword) { + if ($spaceLeft > 20) { + $len = $spaceLeft; + if ($isUtf8) { + $len = MbStringUtils::utf8CharBoundary($word, $len); + } elseif ('=' === \substr($word, $len - 1, 1)) { + --$len; + } elseif ('=' === \substr($word, $len - 2, 1)) { + $len -= 2; + } + + $part = \substr($word, 0, $len); + $word = \substr($word, $len); + $buf .= ' ' . $part; + $message .= $buf . \sprintf('=%s', self::$LE); + } else { + $message .= $buf . $softBreak; + } + + $buf = ''; + } + + while ($word !== '') { + if ($length <= 0) { + break; + } + + $len = $length; + if ($isUtf8) { + $len = MbStringUtils::utf8CharBoundary($word, $len); + } elseif (\substr($word, $len - 1, 1) === '=') { + --$len; + } elseif (\substr($word, $len - 2, 1) === '=') { + $len -= 2; + } + + $part = \substr($word, 0, $len); + $word = (string) \substr($word, $len); + + if ($word !== '') { + $message .= $part . \sprintf('=%s', self::$LE); + } else { + $buf = $part; + } + } + } else { + $bufO = $buf; + if (!$firstword) { + $buf .= ' '; + } + + $buf .= $word; + if ('' !== $bufO && \strlen($buf) > $length) { + $message .= $bufO . $softBreak; + $buf = $word; + } + } + + $firstword = false; + } + + $message .= $buf . self::$LE; + } + + return $message; + } + + /** + * Encode a header value (not including its label) optimally. + * Picks shortest of Q, B, or none. Result includes folding if needed. + * + * @param string $str Header value + * @param string $position Context + * + * @return string + * + * @since 1.0.0 + */ + public function encodeHeader(string $str, string $position = 'text') : string + { + $matchcount = 0; + switch (\strtolower($position)) { + case 'phrase': + if (!\preg_match('/[\200-\377]/', $str)) { + $encoded = \addcslashes($str, "\0..\37\177\\\""); + + return $str === $encoded && !\preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str) + ? $encoded + : "\"$encoded\""; + } + + $matchcount = \preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); + break; + /* @noinspection PhpMissingBreakStatementInspection */ + case 'comment': + $matchcount = \preg_match_all('/[()"]/', $str, $matches); + case 'text': + default: + $matchcount += \preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); + break; + } + + $charset = ((bool) \preg_match('/[\x80-\xFF]/', $str)) ? $this->charset : CharsetType::ASCII; + + // Q/B encoding adds 8 chars and the charset ("` =?
and must not be empty
+ * will look for an image file in $basedir/images/a.png and convert it to inline.
+ * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
+ * Converts data-uri images into embedded attachments.
+ *
+ * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
+ *
+ * @param string $message HTML message string
+ * @param string $basedir Absolute path to a base directory to prepend to relative paths to images
+ * @param null|\Closure $advanced Internal or external text to html converter
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ public function msgHTML(string $message, string $basedir = '', \Closure $advanced = null)
+ {
+ \preg_match_all('/(? 1 && \substr($basedir, -1) !== '/') {
+ $basedir .= '/';
+ }
+
+ foreach ($images[2] as $imgindex => $url) {
+ // Convert data URIs into embedded images
+ $match = [];
+ if (\preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
+ if (\count($match) === 4 && EncodingType::E_BASE64 === $match[2]) {
+ $data = \base64_decode($match[3]);
+ } elseif ('' === $match[2]) {
+ $data = \rawurldecode($match[3]);
+ } else {
+ continue;
+ }
+
+ $cid = \substr(\hash('sha256', $data), 0, 32) . '@phpoms.0'; // RFC2392 S 2
+ if (!$this->cidExists($cid)) {
+ $this->addStringEmbeddedImage($data, $cid, 'embed' . $imgindex, EncodingType::E_BASE64, $match[1]);
+ }
+
+ $message = \str_replace($images[0][$imgindex], $images[1][$imgindex] . '="cid:' . $cid . '"', $message);
+
+ continue;
+ }
+
+ if (!empty($basedir)
+ && (\strpos($url, '..') === false)
+ && \strpos($url, 'cid:') !== 0
+ && !\preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
+ ) {
+ $filename = FileUtils::mb_pathinfo($url, \PATHINFO_BASENAME);
+ $directory = \dirname($url);
+
+ if ('.' === $directory) {
+ $directory = '';
+ }
+
+ // RFC2392 S 2
+ $cid = \substr(\hash('sha256', $url), 0, 32) . '@phpoms.0';
+ if (\strlen($basedir) > 1 && '/' !== \substr($basedir, -1)) {
+ $basedir .= '/';
+ }
+
+ if (\strlen($directory) > 1 && '/' !== \substr($directory, -1)) {
+ $directory .= '/';
+ }
+
+ if ($this->addEmbeddedImage(
+ $basedir . $directory . $filename,
+ $cid,
+ $filename,
+ EncodingType::E_BASE64,
+ MimeType::extensionToMime((string) FileUtils::mb_pathinfo($filename, \PATHINFO_EXTENSION))
+ )
+ ) {
+ $message = \preg_replace(
+ '/' . $images[1][$imgindex] . '=["\']' . \preg_quote($url, '/') . '["\']/Ui',
+ $images[1][$imgindex] . '="cid:' . $cid . '"',
+ $message
+ );
+ }
+ }
+ }
+ }
+
+ $this->contentType = MimeType::M_HTML;
+ $this->body = self::normalizeBreaks($message, self::$LE);
+ $this->bodyAlt = self::normalizeBreaks($this->html2text($message, $advanced), self::$LE);
+
+ if (empty($this->bodyAlt)) {
+ // @todo: localize
+ $this->bodyAlt = 'This is an HTML-only message. To view it, activate HTML in your email application.' . self::$LE;
+ }
+
+ return $this->body;
+ }
+
+ /**
+ * Normalize line breaks in a string.
+ *
+ * @param string $text
+ * @param string $breaktype What kind of line break to use; defaults to self::$LE
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ private static function normalizeBreaks($text, $breaktype) : string
+ {
+ $text = \str_replace(["\r\n", "\r"], "\n", $text);
+
+ if ($breaktype !== "\n") {
+ $text = \str_replace("\n", $breaktype, $text);
+ }
+
+ return $text;
+ }
+
+ /**
+ * Convert an HTML string into plain text.
+ *
+ * @param string $html The HTML text to convert
+ * @param null|\Closure $advanced Internal or external text to html converter
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ private function html2text(string $html, \Closure $advanced = null) : string
+ {
+ if ($advanced !== null) {
+ return $advanced($html);
+ }
+
+ return \html_entity_decode(
+ \trim(\strip_tags(\preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
+ \ENT_QUOTES,
+ $this->charset
+ );
+ }
+
+ /**
+ * Set the public and private key files and password for S/MIME signing.
+ *
+ * @param string $certFile Certification file
+ * @param string $keyFile Key file
+ * @param string $keyPass Password for private key
+ * @param string $extracertsFile Optional path to chain certificate
+ *
+ * @return void
+ *
+ * @since 1.0.0
+ */
+ public function sign($certFile, $keyFile, $keyPass, $extracertsFile = '') : void
+ {
+ $this->signCertFile = $certFile;
+ $this->signKeyFile = $keyFile;
+ $this->signKeyPass = $keyPass;
+ $this->signExtracertFiles = $extracertsFile;
+ }
+
+ /**
+ * Quoted-Printable-encode a DKIM header.
+ *
+ * @param string $txt Text
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ public function dkimQP(string $txt) : string
+ {
+ $line = '';
+ $len = \strlen($txt);
+
+ for ($i = 0; $i < $len; ++$i) {
+ $ord = \ord($txt[$i]);
+ $line .= ((0x21 <= $ord) && ($ord <= 0x3A)) || $ord === 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))
+ ? $txt[$i]
+ : '=' . \sprintf('%02X', $ord);
+ }
+
+ return $line;
+ }
+
+ /**
+ * Generate a DKIM signature.
+ *
+ * @param string $signHeader
+ *
+ * @return string The DKIM signature value
+ *
+ * @since 1.0.0
+ */
+ public function dkimSign(string $signHeader) : string
+ {
+ if (!defined('PKCS7_TEXT')) {
+ return '';
+ }
+
+ $privKeyStr = !empty($this->dkimPrivateKey)
+ ? $this->dkimPrivateKey
+ : \file_get_contents($this->dkimPrivatePath);
+
+ $privKey = $this->dkimPass !== ''
+ ? \openssl_pkey_get_private($privKeyStr, $this->dkimPass)
+ : \openssl_pkey_get_private($privKeyStr);
+
+ if (\openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
+ return \base64_encode($signature);
+ }
+
+ return '';
+ }
+
+ /**
+ * Generate a DKIM canonicalization header.
+ *
+ * @param string $signHeader Header
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ public function dkimHeaderC(string $signHeader) : string
+ {
+ $signHeader = self::normalizeBreaks($signHeader, "\r\n");
+ $signHeader = \preg_replace('/\r\n[ \t]+/', ' ', $signHeader);
+ $lines = \explode("\r\n", $signHeader);
+
+ foreach ($lines as $key => $line) {
+ if (\strpos($line, ':') === false) {
+ continue;
+ }
+
+ list($heading, $value) = \explode(':', $line, 2);
+ $heading = \strtolower($heading);
+ $value = \preg_replace('/[ \t]+/', ' ', $value);
+
+ $lines[$key] = \trim($heading, " \t") . ':' . \trim($value, " \t");
+ }
+
+ return \implode("\r\n", $lines);
+ }
+
+ /**
+ * Generate a DKIM canonicalization body.
+ *
+ * @param string $body Message Body
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ public function dkimBodyC(string $body) : string
+ {
+ if (empty($body)) {
+ return "\r\n";
+ }
+
+ $body = self::normalizeBreaks($body, "\r\n");
+
+ return \rtrim($body, " \r\n\t") . "\r\n";
+ }
+
+ /**
+ * Create the DKIM header and body in a new message header.
+ *
+ * @param string $headersLine Header lines
+ * @param string $subject Subject
+ * @param string $body Body
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ public function dkimAdd($headersLine, $subject, $body) : string
+ {
+ $DKIMsignatureType = 'rsa-sha256';
+ $DKIMcanonicalization = 'relaxed/simple';
+ $DKIMquery = 'dns/txt';
+ $DKIMtime = \time();
+
+ $autoSignHeaders = [
+ 'from',
+ 'to',
+ 'cc',
+ 'date',
+ 'subject',
+ 'reply-to',
+ 'message-id',
+ 'content-type',
+ 'mime-version',
+ 'x-mailer',
+ ];
+
+ if (\stripos($headersLine, 'Subject') === false) {
+ $headersLine .= 'Subject: ' . $subject . self::$LE;
+ }
+
+ $headerLines = \explode(self::$LE, $headersLine);
+ $currentHeaderLabel = '';
+ $currentHeaderValue = '';
+ $parsedHeaders = [];
+ $headerLineIndex = 0;
+ $headerLineCount = \count($headerLines);
+
+ foreach ($headerLines as $headerLine) {
+ $matches = [];
+ if (\preg_match('/^([^ \t]*?)(?::[ \t]*)(.*)$/', $headerLine, $matches)) {
+ if ($currentHeaderLabel !== '') {
+ $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
+ }
+
+ $currentHeaderLabel = $matches[1];
+ $currentHeaderValue = $matches[2];
+ } elseif (\preg_match('/^[ \t]+(.*)$/', $headerLine, $matches)) {
+ $currentHeaderValue .= ' ' . $matches[1];
+ }
+
+ ++$headerLineIndex;
+
+ if ($headerLineIndex >= $headerLineCount) {
+ $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
+ }
+ }
+
+ $copiedHeaders = [];
+ $headersToSignKeys = [];
+ $headersToSign = [];
+
+ foreach ($parsedHeaders as $header) {
+ if (\in_array(\strtolower($header['label']), $autoSignHeaders, true)) {
+ $headersToSignKeys[] = $header['label'];
+ $headersToSign[] = $header['label'] . ': ' . $header['value'];
+
+ if ($this->dkimCopyHeader) {
+ $copiedHeaders[] = $header['label'] . ':'
+ . \str_replace('|', '=7C', $this->dkimQP($header['value']));
+ }
+
+ continue;
+ }
+
+ if (\in_array($header['label'], $this->dkimHeaders, true)) {
+ foreach ($this->customHeader as $customHeader) {
+ if ($customHeader[0] === $header['label']) {
+ $headersToSignKeys[] = $header['label'];
+ $headersToSign[] = $header['label'] . ': ' . $header['value'];
+
+ if ($this->dkimCopyHeader) {
+ $copiedHeaders[] = $header['label'] . ':'
+ . \str_replace('|', '=7C', $this->dkimQP($header['value']));
+ }
+
+ continue 2;
+ }
+ }
+ }
+ }
+
+ $copiedHeaderFields = '';
+ if ($this->dkimCopyHeader && \count($copiedHeaders) > 0) {
+ $copiedHeaderFields = ' z=';
+ $first = true;
+
+ foreach ($copiedHeaders as $copiedHeader) {
+ if (!$first) {
+ $copiedHeaderFields .= self::$LE . ' |';
+ }
+
+ $copiedHeaderFields .= \strlen($copiedHeader) > self::STD_LINE_LENGTH - 3
+ ? \substr(
+ \chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, self::$LE . self::FWS),
+ 0,
+ -\strlen(self::$LE . self::FWS)
+ )
+ : $copiedHeader;
+
+ $first = false;
+ }
+
+ $copiedHeaderFields .= ';' . self::$LE;
+ }
+
+ $headerKeys = ' h=' . \implode(':', $headersToSignKeys) . ';' . self::$LE;
+ $headerValues = \implode(self::$LE, $headersToSign);
+ $body = $this->dkimBodyC($body);
+ $DKIMb64 = \base64_encode(\pack('H*', \hash('sha256', $body)));
+ $ident = '';
+
+ if ($this->dkimIdentity !== '') {
+ $ident = ' i=' . $this->dkimIdentity . ';' . self::$LE;
+ }
+
+ $dkimSignatureHeader = 'DKIM-Signature: v=1;'
+ . ' d=' . $this->dkimDomain . ';'
+ . ' s=' . $this->dkimSelector . ';' . self::$LE
+ . ' a=' . $DKIMsignatureType . ';'
+ . ' q=' . $DKIMquery . ';'
+ . ' t=' . $DKIMtime . ';'
+ . ' c=' . $DKIMcanonicalization . ';' . self::$LE
+ . $headerKeys . $ident . $copiedHeaderFields
+ . ' bh=' . $DKIMb64 . ';' . self::$LE
+ . ' b=';
+
+ $canonicalizedHeaders = $this->dkimHeaderC(
+ $headerValues . self::$LE . $dkimSignatureHeader
+ );
+
+ $signature = $this->dkimSign($canonicalizedHeaders);
+ $signature = \trim(\chunk_split($signature, self::STD_LINE_LENGTH - 3, self::$LE . self::FWS));
+
+ return self::normalizeBreaks($dkimSignatureHeader . $signature, self::$LE);
+ }
+}
diff --git a/Message/Mail/EmailAbstract.php b/Message/Mail/EmailAbstract.php
deleted file mode 100644
index 78964b53a..000000000
--- a/Message/Mail/EmailAbstract.php
+++ /dev/null
@@ -1,669 +0,0 @@
-host = $host;
- $this->port = $port;
- $this->timeout = $timeout;
- $this->ssl = $ssl;
-
- \imap_timeout(\IMAP_OPENTIMEOUT, $timeout);
- \imap_timeout(\IMAP_READTIMEOUT, $timeout);
- \imap_timeout(\IMAP_WRITETIMEOUT, $timeout);
- \imap_timeout(\IMAP_CLOSETIMEOUT, $timeout);
- }
-
- /**
- * Decode
- *
- * @param string $content Content to decode
- * @param int $encoding Encoding type
- *
- * @return string
- *
- * @since 1.0.0
- */
- public static function decode(string $content, string $encoding)
- {
- if ($encoding === EncodingType::E_BASE64) {
- return \imap_base64($content);
- } elseif ($encoding === EncodingType::E_8BIT) {
- return \imap_8bit($content);
- }
-
- return \imap_qprint($content);
- }
-
- /**
- * Descrutor
- *
- * @since 1.0.0
- */
- public function __destruct()
- {
- $this->disconnect();
- }
-
- /**
- * Disconnect server
- *
- * @return void
- *
- * @since 1.0.0
- */
- public function disconnect() : void
- {
- if ($this->con !== null) {
- \imap_close($this->con);
- $this->con = null;
- }
- }
-
- /**
- * Connect to server
- *
- * @param string $user Username
- * @param string $pass Password
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function connect(string $user = '', string $pass = '') : bool
- {
- $this->mailbox = \substr($this->mailbox, 0, -1) . ($this->ssl ? '/ssl/validate-cert' : '/novalidate-cert') . '}';
-
- try {
- $this->con = \imap_open($this->mailbox . 'INBOX', $user, $pass);
-
- return true;
- } catch (\Throwable $t) {
- $this->con = null;
-
- return false;
- }
- }
-
- /**
- * Test connection
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function isConnected() : bool
- {
- return $this->con === null ? false : \imap_ping($this->con);
- }
-
- /**
- * Set submit type/software
- *
- * @param string $submitType Submit type/software
- *
- * @return void
- *
- * @since 1.0.0
- */
- public function setSubmitType(string $submitType) : void
- {
- $this->submitType = $submitType;
-
- if ($this->submitType === SubmitType::SMTP
- || $this->submitType === SubmitType::MAIL
- ) {
- $this->endOfLine = $this->submitType === SubmitType::SMTP || !\stripos(\PHP_OS, 'WIN') === 0 ? \PHP_EOL : "\r\n";
- } elseif ($this->submitType === SubmitType::SENDMAIL) {
- $this->endOfLine = \PHP_EOL;
- $path = \ini_get('sendmail_path');
-
- $this->sendmailPath = \stripos($path, 'sendmail') === false ? '/usr/sbin/sendmail' : $path;
- } elseif ($this->submitType === SubmitType::QMAIL) {
- $this->endOfLine = \PHP_EOL;
- $path = \ini_get('sendmail_path');
-
- $this->sendmailPath = \stripos($path, 'qmail') === false ? '/var/qmail/bin/qmail-inject' : $path;
- }
- }
-
- /**
- * Send a Mail
- *
- * @param Mail $mail Mail to send
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function send(Mail $mail) : bool
- {
- if (empty($mail->getTo())) {
- return false;
- }
-
- $this->preSend($mail);
- $this->postSend($mail);
-
- return true;
- }
-
- private function preSend(Mail $mail) : bool
- {
- $this->endOfLine = $this->submitType === SubmitType::SMTP
- || ($this->submitType === SubmitType::MAIL && \stripos(\PHP_OS, 'WIN') === 0)
- ? "\r\n" : \PHP_EOL;
-
- $this->setMessageType();
-
- if ($this->submitType === SubmitType::MAIL) {
- $this->header .= 'to: ' . $this->addrAppend('to', $this->to);
- $this->header .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->subject)));
- }
-
- return true;
- }
-
- private function postSend(Mail $mail) : bool
- {
- return true;
- }
-
- /**
- * Get boxes.
- *
- * @param string $pattern Pattern for boxes
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getBoxes(string $pattern = '*') : array
- {
- $list = \imap_list($this->con, $this->host, $pattern);
-
- return $list === false ? [] : $list;
- }
-
- /**
- * Get inbox quota.
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getQuota() : array
- {
- $quota = [];
-
- try {
- $quota = \imap_get_quotaroot($this->con, "INBOX");
- } finally {
- return $quota === false ? [] : $quota;
- }
- }
-
- /**
- * Get email.
- *
- * @param string $id mail id
- *
- * @return Mail
- *
- * @since 1.0.0
- */
- public function getEmail(string $id) : Mail
- {
- $mail = new Mail($id);
-
- if ((int) $id > $this->countMessages()) {
- return $mail;
- }
-
- $mail->setOverview(\imap_fetch_overview($this->con, $id));
- $mail->setBody(\imap_fetchbody($this->con, (int) $id, '1.1'));
- //$mail->setEncoding(\imap_fetchstructure($this->con, (int) $id));
-
- return $mail;
- }
-
- /**
- * Get all inbox messages.
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxAll() : array
- {
- return $this->getInboxOverview('ALL');
- }
-
- /**
- * Get inbox overview.
- *
- * @param string $option Inbox option (imap_search creterias)
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxOverview(string $option = 'ALL') : array
- {
- $ids = \imap_search($this->con, $option, \SE_FREE, 'UTF-8');
-
- return \is_array($ids) ? \imap_fetch_overview($this->con, \implode(',', $ids)) : [];
- }
-
- /**
- * Get all new inbox messages.
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxNew() : array
- {
- return $this->getInboxOverview('NEW');
- }
-
- /**
- * Get all inbox messages from a person.
- *
- * @param string $from Messages from
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxFrom(string $from) : array
- {
- return $this->getInboxOverview('FROM "' . $from . '"');
- }
-
- /**
- * Get all inbox messages to a person.
- *
- * @param string $to Messages to
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxTo(string $to) : array
- {
- return $this->getInboxOverview('TO "' . $to . '"');
- }
-
- /**
- * Get all inbox messages cc a person.
- *
- * @param string $cc Messages cc
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxCc(string $cc) : array
- {
- return $this->getInboxOverview('CC "' . $cc . '"');
- }
-
- /**
- * Get all inbox messages bcc a person.
- *
- * @param string $bcc Messages bcc
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxBcc(string $bcc) : array
- {
- return $this->getInboxOverview('BCC "' . $bcc . '"');
- }
-
- /**
- * Get all answered inbox messages.
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxAnswered() : array
- {
- return $this->getInboxOverview('ANSWERED');
- }
-
- /**
- * Get all inbox messages with a certain subject.
- *
- * @param string $subject Subject
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxSubject(string $subject) : array
- {
- return $this->getInboxOverview('SUBJECT "' . $subject . '"');
- }
-
- /**
- * Get all inbox messages from a certain date onwards.
- *
- * @param \DateTime $since Messages since
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxSince(\DateTime $since) : array
- {
- return $this->getInboxOverview('SINCE "' . $since->format('d-M-Y') . '"');
- }
-
- /**
- * Get all unseen inbox messages.
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxUnseen() : array
- {
- return $this->getInboxOverview('UNSEEN');
- }
-
- /**
- * Get all seen inbox messages.
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxSeen() : array
- {
- return $this->getInboxOverview('SEEN');
- }
-
- /**
- * Get all deleted inbox messages.
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxDeleted() : array
- {
- return $this->getInboxOverview('DELETED');
- }
-
- /**
- * Get all inbox messages with text.
- *
- * @param string $text Text in message body
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getInboxText(string $text) : array
- {
- return $this->getInboxOverview('TEXT "' . $text . '"');
- }
-
- /**
- * Create mailbox
- *
- * @param string $mailbox Mailbox to create
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function createMailbox(string $mailbox) : bool
- {
- return \imap_createmailbox($this->con, $mailbox);
- }
-
- /**
- * Rename mailbox
- *
- * @param string $old Old mailbox name
- * @param string $new New mailbox name
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function renameMailbox(string $old, string $new) : bool
- {
- return \imap_renamemailbox($this->con, $old, $new);
- }
-
- /**
- * Delete mailbox
- *
- * @param string $mailbox Mailbox to delete
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function deleteMailbox(string $mailbox) : bool
- {
- return \imap_deletemailbox($this->con, $mailbox);
- }
-
- /**
- * Check message to delete
- *
- * @param int $id Message id
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function deleteMessage(int $id) : bool
- {
- return \imap_delete($this->con, $id);
- }
-
- /**
- * Delete all marked messages
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function deleteMarkedMessages() : bool
- {
- return \imap_expunge($this->con);
- }
-
- /**
- * Check message to delete
- *
- * @param int $length Amount of message overview
- * @param int $start Start index of the overview for pagination
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getMessageOverview(int $length = 0, int $start = 1) : array
- {
- if ($length === 0) {
- $info = \imap_check($this->con);
- $length = $info->Nmsgs;
- }
-
- return \imap_fetch_overview($this->con, $start . ':' . ($length - 1 + $start), 0);
- }
-
- /**
- * Count messages
- *
- * @return int
- *
- * @since 1.0.0
- */
- public function countMessages() : int
- {
- return \imap_num_msg($this->con);
- }
-
- /**
- * Get message header
- *
- * @param int $id Message id
- *
- * @return string
- *
- * @since 1.0.0
- */
- public function getMessageHeader(int $id) : string
- {
- if ($id > $this->countMessages()) {
- return '';
- }
-
- return \imap_fetchheader($this->con, $id);
- }
-
- /**
- * Set the OAuth connection
- *
- * @param OAuth $oauth OAuth
- *
- * @return void
- *
- * @since 1.0.0
- */
- public function setOAuth($oauth) : void
- {
- $this->oauth = $oauth;
- }
-}
diff --git a/Message/Mail/EncodingType.php b/Message/Mail/EncodingType.php
index 763d30462..82b15d09e 100644
--- a/Message/Mail/EncodingType.php
+++ b/Message/Mail/EncodingType.php
@@ -19,7 +19,7 @@ use phpOMS\Stdlib\Base\Enum;
/**
* Encoding enum.
*
- * @package phpOMS\Message\Mail
+ * @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
diff --git a/Message/Mail/EncryptionType.php b/Message/Mail/EncryptionType.php
index 5c0a339d1..b60c818ca 100644
--- a/Message/Mail/EncryptionType.php
+++ b/Message/Mail/EncryptionType.php
@@ -19,14 +19,16 @@ use phpOMS\Stdlib\Base\Enum;
/**
* Encryption enum.
*
- * @package phpOMS\Message\Mail
+ * @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
*/
abstract class EncryptionType extends Enum
{
+ public const NONE = '';
+
public const TLS = 'tls';
- public const SSL = 'ssl';
+ public const SMTPS = 'ssl';
}
diff --git a/Message/Mail/HeaderContext.php b/Message/Mail/HeaderContext.php
index 0645f2da7..4e0f29984 100644
--- a/Message/Mail/HeaderContext.php
+++ b/Message/Mail/HeaderContext.php
@@ -19,7 +19,7 @@ use phpOMS\Stdlib\Base\Enum;
/**
* Submit enum.
*
- * @package phpOMS\Message\Mail
+ * @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
diff --git a/Message/Mail/ICALMethodType.php b/Message/Mail/ICALMethodType.php
index f4213f52b..339effc48 100644
--- a/Message/Mail/ICALMethodType.php
+++ b/Message/Mail/ICALMethodType.php
@@ -19,7 +19,7 @@ use phpOMS\Stdlib\Base\Enum;
/**
* Calendar message types enum.
*
- * @package phpOMS\Message\Mail
+ * @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
diff --git a/Message/Mail/Imap.php b/Message/Mail/Imap.php
index 839db59bf..b9088b779 100644
--- a/Message/Mail/Imap.php
+++ b/Message/Mail/Imap.php
@@ -22,22 +22,142 @@ namespace phpOMS\Message\Mail;
* @link https://orange-management.org
* @since 1.0.0
*/
-class Imap extends EmailAbstract
+class Imap extends MailHandler implements MailBoxInterface
{
/**
- * Connect to server
+ * Destructor.
*
- * @param string $user Username
- * @param string $pass Password
+ * @since 1.0.0
+ */
+ public function __destruct()
+ {
+ $this->inboxClose();
+ parent::__destruct();
+ }
+
+ /**
+ * Connect to server
*
* @return bool
*
* @since 1.0.0
*/
- public function connect(string $user = '', string $pass = '') : bool
+ public function connectInbox() : bool
{
- $this->mailbox = '{' . $this->host . ':' . $this->port . '/imap}';
+ $this->mailbox = ($tmp = \imap_open(
+ '{'
+ . $this->host . ':' . $this->port . '/imap'
+ . ($this->encryption !== EncryptionType::NONE ? '/ssl' : '')
+ . '}',
+ $this->username, $this->password
+ ) === false) ? null : $tmp;
+ }
- return parent::connect($user, $pass);
+ public function getBoxes() : array
+ {
+ $list = \imap_list($this->mailbox, '{' . $this->host . ':' . $this->port . '}');
+ if (!\is_array($list)) {
+ return [];
+ }
+
+ foreach ($list as $key => $value) {
+ $list[$key] = \imap_utf7_decode($value);
+ }
+
+ return $list;
+ }
+
+ public function renameBox(string $old, string $new) : bool
+ {
+ return \imap_renamemailbox($this->mailbox, $old, \imap_utf7_encode($new));
+ }
+
+ public function deleteBox(string $box) : bool
+ {
+ return \imap_deletemailbox($this->mailbox, $box);
+ }
+
+ public function createBox(string $box) : bool
+ {
+ return \imap_createmailbox($this->mailbox, \imap_utf7_encode($box));
+ }
+
+ public function countMail(string $box) : int
+ {
+ if ($this->box !== $box) {
+ \imap_reopen($this->box, $box);
+ $this->box = $box;
+ }
+
+ return \imap_num_msg($this->mailbox);
+ }
+
+ public function getMailboxInfo(string $box) : object
+ {
+ if ($this->box !== $box) {
+ \imap_reopen($this->box, $box);
+ $this->box = $box;
+ }
+
+ return \imap_status($this->mailbox);
+ }
+
+ public function getRecentCount(string $box) : int
+ {
+ if ($this->box !== $box) {
+ \imap_reopen($this->box, $box);
+ $this->box = $box;
+ }
+
+ return \imap_num_recent($this->mailbox);
+ }
+
+ public function copyMail(string|array $messages, string $box) : bool
+ {
+ return \imap_mail_copy($this->mailbox, !\is_string($messages) ? \implode(',', $messages) : $messages, $box);
+ }
+
+ public function moveMail(string|array $messages, string $box) : bool
+ {
+ return \imap_mail_copy($this->mailbox, !\is_string($messages) ? \implode(',', $messages) : $messages, $box);
+ }
+
+ public function deleteMail(int $msg) : bool
+ {
+ return \imap_delete($this->mailbox, $msg);
+ }
+
+ public function getHeaders(string $box) : array
+ {
+ if ($this->box !== $box) {
+ \imap_reopen($this->box, $box);
+ $this->box = $box;
+ }
+
+ return \imap_headers($this->mailbox);
+ }
+
+ public function getHeaderInfo(int $msg) : object
+ {
+ return \imap_headerinfo($this->mailbox, $msg);
+ }
+
+ public function getMail(int $msg) : Email
+ {
+ return new Email();
+ }
+
+ /**
+ * Close mailbox
+ *
+ * @return void
+ *
+ * @since 1.0.0
+ */
+ public function inboxClose() : void
+ {
+ if ($this->mailbox !== null) {
+ \imap_close($this->mailbox);
+ }
}
}
diff --git a/Message/Mail/Mail.php b/Message/Mail/Mail.php
deleted file mode 100644
index 469617a80..000000000
--- a/Message/Mail/Mail.php
+++ /dev/null
@@ -1,1511 +0,0 @@
-id = $id;
- }
-
- /**
- * Set body.
- *
- * @param string $body Mail body
- *
- * @return void
- *
- * @since 1.0.0
- */
- public function setBody(string $body) : void
- {
- $this->body = $body;
- }
-
- /**
- * Set body alt.
- *
- * @param string $body Mail body
- *
- * @return void
- *
- * @since 1.0.0
- */
- public function setBodyAlt(string $body) : void
- {
- $this->bodyAlt = $body;
- $this->contentType = empty($body) ? MimeType::M_TXT : MimeType::M_ALT;
- }
-
- /**
- * Set body.
- *
- * @param array $overview Mail overview
- *
- * @return void
- *
- * @since 1.0.0
- */
- public function setOverview(array $overview) : void
- {
- $this->overview = $overview;
- }
-
- /**
- * Set encoding.
- *
- * @param int $encoding Mail encoding
- *
- * @return void
- *
- * @since 1.0.0
- */
- public function setEncoding(int $encoding) : void
- {
- $this->encoding = $encoding;
- }
-
- /**
- * Set content type.
- *
- * @param int $contentType Mail content type
- *
- * @return void
- *
- * @since 1.0.0
- */
- public function setContentType(int $contentType) : void
- {
- $this->contentType = empty($this->bodyAlt) ? $contentType : MimeType::M_ALT;
- }
-
- /**
- * Set subject
- *
- * @param string $subject Subject
- *
- * @return void
- *
- * @since 1.0.0
- */
- public function setSubject(string $subject) : void
- {
- $this->subject = \trim($subject);
- }
-
- /**
- * Set the from address
- *
- * @param string $mail Mail address
- * @param string $name Name
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function setFrom(string $mail, string $name = '') : bool
- {
- $mail = $this->normalizeEmailAddress($mail);
- $name = $this->normalizeName($name);
-
- if ($mail === null) {
- return false;
- }
-
- $this->from = $mail;
- $this->fromName = $name;
-
- return true;
- }
-
- /**
- * Add a to address
- *
- * @param string $mail Mail address
- * @param string $name Name
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function addTo(string $mail, string $name = '') : bool
- {
- $mail = $this->normalizeEmailAddress($mail);
- $name = \trim($name);
-
- if ($mail === null) {
- return false;
- }
-
- $this->to[$mail] = $name;
-
- return true;
- }
-
- /**
- * Get to addresses
- *
- * @return array
- *
- * @since 1.0.0
- */
- public function getTo() : array
- {
- return $this->to;
- }
-
- /**
- * Add a cc address
- *
- * @param string $mail Mail address
- * @param string $name Name
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function addCc(string $mail, string $name = '') : bool
- {
- $mail = $this->normalizeEmailAddress($mail);
- $name = \trim($name);
-
- if ($mail === null) {
- return false;
- }
-
- $this->cc[$mail] = $name;
-
- return true;
- }
-
- /**
- * Add a bcc address
- *
- * @param string $mail Mail address
- * @param string $name Name
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function addBcc(string $mail, string $name = '') : bool
- {
- $mail = $this->normalizeEmailAddress($mail);
- $name = \trim($name);
-
- if ($mail === null) {
- return false;
- }
-
- $this->bcc[$mail] = $name;
-
- return true;
- }
-
- /**
- * Add an attachment
- *
- * @param string $path Path to the file
- * @param string $name Name of the file
- * @param string $encoding Encoding
- * @param string $type Mime type
- * @param string $disposition Disposition
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function addAttachment(
- string $path,
- string $name = '',
- string $encoding = EncodingType::E_BASE64,
- string $type = '',
- string $disposition = DispositionType::ATTACHMENT
- ) : bool {
- if ((bool) \preg_match('#^[a-z]+://#i', $path)) {
- return false;
- }
-
- $this->attachment[] = [
- 'path' => $path,
- 'filename' => \basename($path),
- 'name' => $name,
- 'encoding' => $encoding,
- 'type' => $type,
- 'string' => false,
- 'disposition' => $disposition,
- 'id' => $name,
- ];
-
- return true;
- }
-
- /**
- * Add string attachment
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function addStringAttachment(
- string $string,
- string $filename,
- string $encoding = EncodingType::E_BASE64,
- string $type = '',
- string $disposition = DispositionType::ATTACHMENT
- ) : bool {
- $type = $type === '' ? MimeType::getByName('M_' . \strtoupper(File::extension($filename))) : $type;
-
- $this->attachment[] = [
- 'path' => $string,
- 'filename' => $filename,
- 'name' => \basename($filename),
- 'encoding' => $encoding,
- 'type' => $type,
- 'string' => true,
- 'disposition' => $disposition,
- 'id' => 0,
- ];
-
- return true;
- }
-
- /**
- * Add inline image
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function addEmbeddedImage(
- string $path,
- string $cid,
- string $name = '',
- string $encoding = EncodingType::E_BASE64,
- string $type = '',
- string $disposition = DispositionType::INLINE
- ) : bool {
- if ((bool) \preg_match('#^[a-z]+://#i', $path)) {
- return false;
- }
-
- $type = $type === '' ? MimeType::getByName('M_' . \strtoupper(File::extension($path))) : $type;
- $filename = \basename($path);
-
- $this->attachment[] = [
- 'path' => $path,
- 'filename' => $filename,
- 'name' => empty($name) ? $filename : $name,
- 'encoding' => $encoding,
- 'type' => $type,
- 'string' => false,
- 'disposition' => $disposition,
- 'id' => $cid,
- ];
-
- return true;
- }
-
- /**
- * Add inline image attachment
- *
- * @return bool
- *
- * @since 1.0.0
- */
- public function addStringEmbeddedImage(
- string $string,
- string $cid,
- string $name = '',
- string $encoding = EncodingType::E_BASE64,
- string $type = '',
- string $disposition = DispositionType::INLINE
- ) : bool {
- $type = $type === '' ? MimeType::getByName('M_' . \strtoupper(File::extension($name))) : $type;
-
- $this->attachment[] = [
- 'path' => $string,
- 'filename' => $name,
- 'name' => $name,
- 'encoding' => $encoding,
- 'type' => $type,
- 'string' => true,
- 'disposition' => $disposition,
- 'id' => $cid,
- ];
-
- return true;
- }
-
- /**
- * The email should be confirmed by the receivers
- *
- * @param string $confirm Should be confirmed?
- *
- * @return void
- *
- * @sicne 1.0.0
- */
- public function shouldBeConfirmed(bool $confirm = true) : void
- {
- $this->confirmReading = $confirm;
- }
-
- /**
- * Normalize an email address
- *
- * @param string $mail Mail address
- *
- * @return null|string
- *
- * @since 1.0.0
- */
- private function normalizeEmailAddress(string $mail) : ?string
- {
- $mail = \trim($mail);
- $pos = \strrpos($mail, '@');
-
- if ($pos === false || !\filter_var($mail, \FILTER_VALIDATE_EMAIL)) {
- return null;
- }
-
- $domain = \substr($mail, ++$pos);
- if (!((bool) \preg_match('/[\x80-\xFF]/', $domain))) {
- return $mail;
- }
-
- $domain = \mb_convert_encoding($domain, 'UTF-8', $this->charset);
- $normalized = \idn_to_ascii($mail);
-
- return $normalized === false ? $mail : \substr($domain, 0, $pos) . $normalized;
- }
-
- /**
- * Normalize an email name
- *
- * @param string $name Name
- *
- * @return string
- *
- * @since1 1.0.0
- */
- private function normalizeName(string $name) : string
- {
- return \trim(\preg_replace("/[\r\n]+/", '', $name));
- }
-
- /**
- * Parsing an email containing a name
- *
- * @param string $mail Mail string
- *
- * @return array
- *
- * @since 1.0.0
- */
- private function parseEmailAddress(string $mail) : array
- {
- $addresses = [];
- $list = \explode(',', $mail);
-
- foreach ($list as $address) {
- $address = \trim($address);
-
- if (\stripos($address, '<') === false) {
- if (($address = $this->normalizeEmailAddress($address)) !== null) {
- $addresses[] = [
- 'name' => '',
- 'address' => $address,
- ];
- }
- } else {
- $parts = \explode('<', $address);
- $address = \trim(\str_replace('>', '', $parts[1]));
-
- if (($address = $this->normalizeEmailAddress($address)) !== null) {
- $addresses[] = [
- 'name' => \trim(\str_replace(['"', '\''], '', $parts[0])),
- 'address' => $address,
- ];
- }
- }
- }
-
- return $addresses;
- }
-
- /**
- * Define the message type based on the content
- *
- * @return void
- *
- * @since 1.0.0
- */
- private function setMessageType() : void
- {
- $this->messageType = '';
-
- $type = [];
- if (!empty($this->bodyAlt)) {
- $type[] = DispositionType::ALT;
- }
-
- foreach ($this->attachment as $attachment) {
- if ($attachment['disposition'] === DispositionType::INLINE) {
- $type[] = DispositionType::INLINE;
- } elseif ($attachment['disposition'] === DispositionType::ATTACHMENT) {
- $type[] = DispositionType::ATTACHMENT;
- }
- }
-
- $this->messageType = \implode('_', $type);
- $this->messageType = empty($this->messageType) ? DispositionType::PLAIN : $this->messageType;
- }
-
- /**
- * Create the mail body
- *
- * @return string
- *
- * @since 1.0.0
- */
- public function createBody() : string
- {
- $this->id = empty($this->id) ? $this->generatedId() : $this->id;
-
- $output = '';
- $boundary = [];
- $boundary[0] = 'b0_' . $this->id;
- $boundary[1] = 'b1_' . $this->id;
- $boundary[2] = 'b2_' . $this->id;
- $boundary[3] = 'b3_' . $this->id;
-
- $output .= !empty($this->signKeyFile) ? $this->generateMimeHeader($boundary) . $this->endOfLine : '';
-
- $body = $this->wrapText($this->body, $this->wordWrap, false);
- $bodyEncoding = $this->encoding;
- $bodyCharset = $this->charset;
-
- if ($bodyEncoding === EncodingType::E_8BIT && !((bool) \preg_match('/[\x80-\xFF]/', $body))) {
- $bodyEncoding = EncodingType::E_7BIT;
- $bodyCharset = CharsetType::ASCII;
- }
-
- if ($this->encoding !== EncodingType::E_BASE64 && ((bool) \preg_match('/^(.{' . (63 + \strlen($this->endOfLine)) . ',})/m', $body))) {
- $bodyEncoding = EncodingType::E_QUOTED;
- }
-
- $bodyAlt = $this->wrapText($this->bodyAlt, $this->wordWrap, false);
- $bodyAltEncoding = $this->encoding;
- $bodyAltCharset = $this->charset;
-
- if ($bodyAlt !== '') {
- if ($bodyAltEncoding === EncodingType::E_8BIT && !((bool) \preg_match('/[\x80-\xFF]/', $bodyAlt))) {
- $bodyAltEncoding = EncodingType::E_7BIT;
- $bodyAltCharset = CharsetType::ASCII;
- }
-
- if ($this->encoding !== EncodingType::E_BASE64 && ((bool) \preg_match('/^(.{' . (63 + \strlen($this->endOfLine)) . ',})/m', $bodyAlt))) {
- $bodyAltEncoding = EncodingType::E_QUOTED;
- }
- }
-
- $mimeBody = 'This is a multi-part message in MIME format.' . $this->endOfLine . $this->endOfLine;
-
- switch ($this->messageType) {
- case DispositionType::INLINE:
- case DispositionType::ATTACHMENT:
- $body .= $mimeBody;
- $body .= $this->getBoundary($boundary[0], $bodyCharset, $this->contentType, $bodyEncoding);
- $body .= $this->encodeString($this->body, $bodyEncoding);
- $body .= $this->endOfLine;
- $body .= $this->attachAll($this->messageType, $boundary[0]);
- break;
- case DispositionType::INLINE . '_' . DispositionType::ATTACHMENT:
- $body .= $mimeBody;
- $body .= '--' . $boundary[0] . $this->endOfLine;
- $body .= 'Content-Type: ' . MimeType::M_RELATED . ';' . $this->endOfLine;
- $body .= ' boundary ="' . $boundary[1] . '";' . $this->endOfLine;
- $body .= ' type ="' . MimeType::M_HTML . '";' . $this->endOfLine;
- $body .= $this->endOfLine;
- $body .= $this->getBoundary($boundary[1], $bodyCharset, $this->contentType, $bodyEncoding);
- $body .= $this->encodeString($this->body, $bodyEncoding);
- $body .= $this->endOfLine;
- $body .= $this->attachAll(DispositionType::INLINE, $boundary[1]);
- $body .= $this->endOfLine;
- $body .= $this->attachAll(DispositionType::ATTACHMENT, $boundary[1]);
- break;
- case DispositionType::ALT:
- $body .= $mimeBody;
- $body .= $this->getBoundary($boundary[0], $bodyAltCharset, MimeType::M_TEXT, $bodyAltEncoding);
- $body .= $this->encodeString($this->bodyAlt, $bodyAltEncoding);
- $body .= $this->endOfLine;
- $body .= $this->getBoundary($boundary[0], $bodyCharset, MimeType::M_HTML, $bodyEncoding);
- $body .= $this->encodeString($this->body, $bodyEncoding);
- $body .= $this->endOfLine;
-
- if (!empty($this->icalBody)) {
- $method = ICALMethodType::REQUEST;
- $constants = ICALMethodType::getConstants();
-
- foreach ($constants as $enum) {
- if (\stripos($this->icalBody, 'METHOD:' . $enum) !== false
- || \stripos($this->icalBody, 'METHOD: ' . $enum) !== false
- ) {
- $method = $enum;
- break;
- }
- }
-
- $body .= $this->getBoundary($boundary[0], $this->charset, MimeType::M_ICS . '; method=' . $method, $this->encoding);
- $body .= $this->encodeString($this->icalBody, $this->encoding);
- $body .= $this->endOfLine;
- }
-
- $body .= $this->endOfLine . '--' . $boundary[0] . '--' . $this->endOfLine;
- break;
- case DispositionType::ALT . '_' . DispositionType::INLINE:
- $body .= $mimeBody;
- $body .= $this->getBoundary($boundary[0], $bodyAltCharset, MimeType::M_TEXT, $bodyAltEncoding);
- $body .= $this->encodeString($this->bodyAlt, $bodyAltEncoding);
- $body .= $this->endOfLine;
- $body .= '--' . $boundary[0] . $this->endOfLine;
- $body .= 'Content-Type: ' . MimeType::M_RELATED . ';' . $this->endOfLine;
- $body .= ' boundary="' . $boundary[1] . '";' . $this->endOfLine;
- $body .= ' type="' . MimeType::M_HTML . '";' . $this->endOfLine;
- $body .= $this->endOfLine;
- $body .= $this->getBoundary($boundary[1], $bodyCharset, MimeType::M_HTML, $bodyEncoding);
- $body .= $this->encodeString($this->body, $bodyEncoding);
- $body .= $this->endOfLine;
- $body .= $this->attachAll(DispositionType::INLINE, $boundary[1]);
- $body .= $this->endOfLine;
- $body .= $this->endOfLine . '--' . $boundary[0] . '--' . $this->endOfLine;
- break;
- case DispositionType::ALT . '_' . DispositionType::ATTACHMENT:
- $body .= $mimeBody;
- $body .= '--' . $boundary[0] . $this->endOfLine;
- $body .= 'Content-Type: ' . MimeType::M_ALT . ';' . $this->endOfLine;
- $body .= ' boundary="' . $boundary[1] . '"' . $this->endOfLine;
- $body .= $this->endOfLine;
- $body .= $this->getBoundary($boundary[1], $bodyAltCharset, MimeType::M_TEXT, $bodyAltEncoding);
- $body .= $this->encodeString($this->bodyAlt, $bodyAltEncoding);
- $body .= $this->endOfLine;
- $body .= $this->getBoundary($boundary[1], $bodyCharset, MimeType::M_HTML, $bodyEncoding);
- $body .= $this->encodeString($this->body, $bodyEncoding);
- $body .= $this->endOfLine;
-
- if (!empty($this->icalBody)) {
- $method = ICALMethodType::REQUEST;
- $constants = ICALMethodType::getConstants();
-
- foreach ($constants as $enum) {
- if (\stripos($this->icalBody, 'METHOD:' . $enum) !== false
- || \stripos($this->icalBody, 'METHOD: ' . $enum) !== false
- ) {
- $method = $enum;
- break;
- }
- }
-
- $body .= $this->getBoundary($boundary[1], $this->charset, MimeType::M_ICS . '; method=' . $method, $this->encoding);
- $body .= $this->encodeString($this->icalBody, $this->encoding);
- }
-
- $body .= $this->endOfLine . '--' . $boundary[1] . '--' . $this->endOfLine;
- $body .= $this->endOfLine;
- $body .= $this->attachAll(DispositionType::ATTACHMENT, $boundary[0]);
- break;
- case DispositionType::ALT . '_' . DispositionType::INLINE . '_' . DispositionType::ATTACHMENT:
- $body .= $mimeBody;
- $body .= '--' . $boundary[0] . $this->endOfLine;
- $body .= 'Content-Type: ' . MimeType::M_ALT . $this->endOfLine;
- $body .= ' boundary="' . $boundary[1] . '"' . $this->endOfLine;
- $body .= $this->endOfLine;
- $body .= $this->getBoundary($boundary[1], $bodyAltCharset, MimeType::M_TEXT, $bodyAltEncoding);
- $body .= $this->encodeString($this->bodyAlt, $bodyAltEncoding);
- $body .= $this->endOfLine;
- $body .= '--' . $boundary[1] . $this->endOfLine;
- $body .= 'Content-Type: ' . MimeType::M_RELATED . ';' . $this->endOfLine;
- $body .= ' boundary="' . $boundary[2] . '"' . $this->endOfLine;
- $body .= ' type="' . MimeType::M_HTML . '"' . $this->endOfLine;
- $body .= $this->endOfLine;
- $body .= $this->getBoundary($boundary[2], $bodyCharset, MimeType::M_HTML, $bodyEncoding);
- $body .= $this->encodeString($this->body, $bodyEncoding);
- $body .= $this->endOfLine;
- $body .= $this->attachAll(DispositionType::INLINE, $boundary[2]);
- $body .= $this->endOfLine;
- $body .= $this->endOfLine . '--' . $boundary[2] . '--' . $this->endOfLine;
- $body .= $this->attachAll(DispositionType::ATTACHMENT, $boundary[1]);
- break;
- default:
- $body .= $this->encodeString($this->body, $bodyEncoding);
- }
-
- if ($this->signKeyFile !== '') {
- // @todo implement
- $output .= '';
- }
-
- return $output;
- }
-
- /**
- * Create html message
- *
- * @return void
- *
- * @since 1.0.0
- */
- public function createHtmlMsg() : void
- {
- }
-
- /**
- * Convert html to text message
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function htmlToText() : string
- {
- return '';
- }
-
- /**
- * Normalize text
- *
- * Line break
- *
- * @param string $text Text to normalized
- * @param string $lb Line break
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function normalizeText(string $text, string $lb = "\n") : string
- {
- return \str_replace(["\r\n", "\r", "\n"], $lb, $text);
- }
-
- /**
- * Generate a random id
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function generatedId() : string
- {
- $rand = '';
-
- try {
- $rand = \random_bytes(32);
- } catch (\Throwable $t) {
- $rand = \hash('sha256', \uniqid((string) \mt_rand(), true), true);
- }
-
- return \base64_encode(\hash('sha256', $rand, true));
- }
-
- /**
- * Generate the mime header
- *
- * @param array $boundary Message boundary
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function generateMimeHeader(array $boundary) : string
- {
- $mime = '';
- $isMultipart = true;
-
- switch ($this->messageType) {
- case DispositionType::INLINE:
- $mime .= 'Content-Type:' . MimeType::M_RELATED . ';' . $this->endOfLine;
- $mime .= ' boundary="' . $boundary[0] . '"' . $this->endOfLine;
- break;
- case DispositionType::ATTACHMENT:
- case DispositionType::INLINE . '_' . DispositionType::ATTACHMENT:
- case DispositionType::ALT . '_' . DispositionType::ATTACHMENT:
- case DispositionType::ALT . '_' . DispositionType::INLINE . '_' . DispositionType::ATTACHMENT:
- $mime .= 'Content-Type:' . MimeType::M_MIXED . ';' . $this->endOfLine;
- $mime .= ' boundary="' . $boundary[0] . '"' . $this->endOfLine;
- break;
- case DispositionType::ALT:
- case DispositionType::ALT . '_' . DispositionType::INLINE:
- $mime .= 'Content-Type:' . MimeType::M_ALT . ';' . $this->endOfLine;
- $mime .= ' boundary="' . $boundary[0] . '"' . $this->endOfLine;
- break;
- default:
- $mime .= 'Content-Type:' . $this->contentType . '; charset=' . CharsetType::UTF_8 . ';' . $this->endOfLine;
-
- $isMultipart = false;
- }
-
- return $isMultipart && $this->encoding !== EncodingType::E_7BIT
- ? 'Content-Transfer-Encoding:' . $this->encoding . ';' . $this->endOfLine
- : $mime;
- }
-
- /**
- * Wrap text
- *
- * @param string $text Text to wrap
- * @param int $length Line length
- * @param bool $quoted Is quoted
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function wrapText(string $text, int $length, bool $quoted = false) : string
- {
- if ($length < 1 || $text === '') {
- return $text;
- }
-
- $softEndOfLine = $quoted ? ' =' . $this->endOfLine : $this->endOfLine;
-
- $text = $this->normalizeText($text, $this->endOfLine);
- $text = \rtrim($text, "\r\n");
-
- $lines = \explode($this->endOfLine, $text);
-
- $buffer = '';
- $output = '';
- $crlfLength = \strlen($this->endOfLine);
- $first = true;
- $isUTF8 = $this->charset === CharsetType::UTF_8;
-
- foreach ($lines as $line) {
- $words = \explode(' ', $line);
-
- foreach ($words as $word) {
- if ($quoted && \strlen($word) > $length) {
- if ($first) {
- $spaces = $length - \strlen($buffer) - $crlfLength;
-
- if ($spaces > 20) {
- $len = $spaces;
- if ($isUTF8) {
- $len = MbStringUtils::utf8CharBoundary($word, $len);
- } elseif ($word[$len - 1] === '=') {
- --$len;
- } elseif ($word[$len - 2] === '=') {
- $len -= 2;
- }
-
- $part = \substr($word, 0, $len);
- $word = \substr($word, $len);
- $output .= $buffer . ' ' . $part . '=' . $this->endOfLine;
- } else {
- $output .= $buffer . $softEndOfLine;
- }
-
- $buffer = '';
- }
-
- while ($word !== '') {
- if ($length < 1) {
- break;
- }
-
- $len = $length;
-
- if ($isUTF8) {
- $len = MbStringUtils::utf8CharBoundary($word, $len);
- } elseif ($word[$len - 1] === '=') {
- --$len;
- } elseif ($word[$len - 2] === '=') {
- $len -= 2;
- }
-
- $part = \substr($word, 0, $len);
- $word = \substr($word, $len);
-
- if ($word !== '') {
- $output .= $part . '=' . $this->endOfLine;
- } else {
- $buffer = $part;
- }
- }
- } else {
- $oldBuf = $buffer;
- $buffer .= $word . ' ';
-
- if (\strlen($buffer) > $length) {
- $output .= \rtrim($oldBuf) . $softEndOfLine;
- $buffer = $word;
- }
- }
- }
-
- $output .= \rtrim($buffer) . $this->endOfLine;
- }
-
- return $output;
- }
-
- /**
- * Render the boundary
- *
- * @param string $boundary Boundary identifier
- * @param string $charset Charset
- * @param string $contentType ContentType
- * @param string $encoding Encoding
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function getBoundary(string $boundary, string $charset = null, string $contentType = null, string $encoding = null) : string
- {
- $boundary = '';
- $charset = empty($charset) ? $this->charset : $charset;
- $contentType = empty($contentType) ? $this->contentType : $contentType;
- $encoding = empty($encoding) ? $this->encoding : $encoding;
-
- $boundary .= '--' . $boundary . $this->endOfLine;
- $boundary .= 'Content-Type: ' . $contentType . '; charset=' . $charset . $this->endOfLine;
-
- if ($encoding !== EncodingType::E_7BIT) {
- $boundary .= 'Content-Transfer-Encoding: ' . $encoding . $this->endOfLine;
- }
-
- return $boundary . $this->endOfLine;
- }
-
- /**
- * Encode a string
- *
- * @param string $text Text to encode
- * @param string $encoding Encoding to use
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function encodeString(string $text, string $encoding = EncodingType::E_BASE64) : string
- {
- $encoded = '';
- if ($encoding === EncodingType::E_BASE64) {
- $encoded = \chunk_split(\base64_encode($text), 76, $this->endOfLine);
- } elseif ($encoding === EncodingType::E_7BIT || $encoding === EncodingType::E_8BIT) {
- $encoded = $this->normalizeText($text, $this->endOfLine);
-
- if (\substr($encoded, -\strlen($this->endOfLine)) !== $this->endOfLine) {
- $encoded .= $this->endOfLine;
- }
- } elseif ($encoding === EncodingType::E_BINARY) {
- $encoded = $text;
- } elseif ($encoded === EncodingType::E_QUOTED) {
- $encoded = $this->normalizeText(\quoted_printable_decode($text));
- }
-
- return $encoded;
- }
-
- /**
- * Attach all attachments
- *
- * @param string $disposition Disposition type
- * @param string $boundary Boundary identifier
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function attachAll(string $disposition, string $boundary) : string
- {
- $mime = [];
- $cid = [];
- $incl = [];
-
- foreach ($this->attachment as $attach) {
- if ($attach['disposition'] === $disposition) {
- $hash = \hash('sha256', \serialize($attach));
- if (\in_array($hash, $incl, true)) {
- continue;
- }
-
- $incl[] = $hash;
-
- if ($attach['disposition'] && isset($cid[$attach['id']])) {
- continue;
- }
-
- $cid[$attach['id']] = true;
-
- $mime[] = '--' . $boundary . $this->endOfLine;
- $mime[] = !empty($attach['name'])
- ? 'Content-Type: ' . $attach['type'] . '; name=' . $this->quotedString($this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $attach['name'])))) . '"' . $this->endOfLine
- : 'Content-Type: ' . $attach['type'] . $this->endOfLine;
-
- if ($attach['encoding'] !== EncodingType::E_7BIT) {
- $mime[] = 'Content-Transfer-Encoding: ' . $attach['encoding'] . $this->endOfLine;
- }
-
- if (((string) $attach['cid']) !== '' && $attach['disposition'] === DispositionType::INLINE) {
- $mime[] = 'Content-ID: <' . $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $attach['cid']))) . '>' . $this->endOfLine;
- }
-
- if (!empty($attach['disposition'])) {
- $encodedName = $this->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $attach['name'])));
-
- // @todo: "" might be wrong for || condition
- $mime[] = !empty($encodedName)
- ? 'Content-Disposition: ' . $attach['disposition'] . '; filename=' . $this->quotedString($encodedName) . $this->endOfLine
- : 'Content-Disposition: ' . $attach['disposition'] . $this->endOfLine;
- }
-
- $mime[] = $this->endOfLine;
- $mime[] = $attach['string'] ? $this->encodeString($attach['path'], $attach['encoding']) : $this->encodeFile($attach['path'], $attach['encoding']);
- $mime[] = $this->endOfLine;
- }
- }
-
- $mime[] = '--' . $boundary . '--' . $this->endOfLine;
-
- return \implode('', $mime);
- }
-
- /**
- * Encode header value
- *
- * @param string $value Value to encode
- * @param int $context Value context
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function encodeHeader(string $value, int $context = HeaderContext::TEXT) : string
- {
- $matches = 0;
- switch ($context) {
- case HeaderContext::PHRASE:
- if (!\preg_match('/[\200-\377]/', $value)) {
- $encoded = \addslashes($value, "\0..\37\177\\\"");
-
- return ($encoded === $value) && !\preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $value) ? $encoded : '"' . $encoded . '"';
- }
-
- $matches = \preg_match_all('/[^\040\041\043-\133\135-\176]/', $value, $matched);
- break;
- case HeaderContext::COMMENT:
- $matches = \preg_match_all('/[()"]/', $value, $matched);
- /* fallthrough */
- default:
- $matches += \preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $value, $matched);
- }
-
- $charset = ((bool) \preg_match('/[\x80-\xFF]/', $value)) ? $this->charset : CharsetType::ASCII;
- $overhead = \strlen($charset) + 8;
- $maxlength = $this->submitType === SubmitType::MAIL ? 63 - $overhead : 998 - $overhead;
-
- $valueLength = \strlen($value);
- $encoded = '';
-
- if ($matches > $valueLength / 3) {
- $encoded = MbStringUtils::hasMultiBytes($value)
- ? $this->base64EncodeWrapMb($value, "\n")
- : \trim(\chunk_split(\base64_encode($value), $maxlength - $maxlength % 4, "\n"));
-
- $encoded = \preg_replace('/^(.*)$/m', ' =?' . $charset . '?B?\\1?=', $encoded);
- } elseif ($matches > 0 || $valueLength > $maxlength) {
- $encoded = $this->encodeQ($value, $context);
- $encoded = $this->wrapText($encoded, $maxlength, true);
- $encoded = \str_replace('=' . $this->endOfLine, "\n", \trim($encoded));
- $encoded = \preg_replace('/^(.*)$/m', ' =?' . $charset . '?Q?\\1?=', $encoded);
- } else {
- return $value;
- }
-
- return \trim($this->normalizeText($encoded));
- }
-
- /**
- * Encode a file
- *
- * @param string $path Path to a file
- * @param string $encoding Encoding of the file
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function encodeFile(string $path, string $encoding = EncodingType::E_BASE64) : string
- {
- if (!\is_readable($path) || (bool) \preg_match('#^[a-z]+://#i', $path)) {
- return '';
- }
-
- $content = \file_get_contents($path);
-
- if ($content === false) {
- return '';
- }
-
- return $this->encodeString($content, $encoding);
- }
-
- /**
- * Encode text as base64 multibye
- *
- * @param string $text Text to encode
- * @param string $lb Linebreak
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function base64EncodeWrapMb(string $text, string $lb = "\n") : string
- {
- $start = '=?' . $this->charset . '?B?';
- $end = '?=';
- $encoded = '';
-
- $mbLength = \mb_strlen($text, $this->charset);
- $length = 75 - \strlen($start) - \strlen($end);
- $ratio = $mbLength / \strlen($text);
- $avgLength = \floor($length * $ratio * 0.75);
-
- $offset = 0;
- $chunk = '';
-
- for ($i = 0; $i < $mbLength; $i += $offset) {
- $lookBack = 0;
-
- do {
- $offset = $avgLength - $lookBack;
- $chunk = \mb_substr($text, $i, $offset, $this->charset);
- $chunk = \base64_encode($chunk);
-
- ++$lookBack;
- } while (\strlen($chunk) > $length);
-
- $encoded .= $chunk . $lb;
- }
-
- return \substr($encoded, 0, -\strlen($lb));
- }
-
- /**
- * Escape special strings
- *
- * @param string $text Text to escape
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function quotedString(string $text) : string
- {
- return \preg_match('/[ ()<>@,;:"\/\[\]?=]/', $text) === false ? $text : '"' . \str_replace('"', '\\"', $text) . '"';
- }
-
- /**
- * Quoted encode
- *
- * @param string $text Text to encode
- * @param int $context Value context
- *
- * @return string
- *
- * @since 1.0.0
- */
- private function encodeQ(string $text, int $context = HeaderContext::TEXT) : string
- {
- $pattern = '';
- switch ($context) {
- case HeaderContext::PHRASE:
- $pattern = '^A-Za-z0-9!*+\/ -';
- break;
- case HeaderContext::COMMENT:
- $pattern = '\(\)"';
- break;
- case HeaderContext::TEXT:
- default:
- $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
- }
-
- $matches = [];
- $encoded = \str_replace(["\r", "\n"], '', $text);
-
- if (\preg_match_all('/[{' . $pattern . '}]/', $encoded, $matches) !== false) {
- $eqkey = \array_search('', $matches[0], true);
- if ($eqkey !== false) {
- unset($matches[0][$eqkey]);
- \array_unshift($matches[0], '=');
- }
-
- $unique = \array_unique($matches[0]);
- foreach ($unique as $char) {
- $encoded = \str_replace($char, '=' . \sprintf('%02X', \ord($char)), $encoded);
- }
- }
-
- return \str_replace(' ', '_', $encoded);
- }
-
- protected function hasCid(string $cid) : bool
- {
- foreach ($this->attachment as $attachment) {
- if ($attachment['disposition'] === DispositionType::INLINE && $attachment['id'] === $cid) {
- return true;
- }
- }
-
- return false;
- }
-
- public function hasInlineImage() : bool
- {
- foreach ($this->attachment as $attachment) {
- if ($attachment['disposition'] === DispositionType::INLINE) {
- return true;
- }
- }
-
- return false;
- }
-
- public function hasAttachment() : bool
- {
- foreach ($this->attachment as $attachmnet) {
- if ($attachment['disposition'] === DispositionType::ATTACHMENT) {
- return true;
- }
- }
-
- return false;
- }
-
- public function hasAlternative() : bool
- {
- return !empty($this->bodyAlt);
- }
-
- public function clearAddresses() : void
- {
- $this->to = [];
- $this->cc = [];
- $this->bcc = [];
- }
-
- public function clearAllCC() : void
- {
- $this->cc = [];
- }
-
- public function clearAllTo() : void
- {
- $this->to = [];
- }
-
- public function clearAllBCC() : void
- {
- $this->bcc = [];
- }
-
- public function clearAttachments() : void
- {
- $this->attachment = [];
- }
-
- public function clearCustomHeaders() : void
- {
- $this->header = [];
- }
-
- protected function serverHostname() : string
- {
- if (!empty($this->hostname)) {
- return $this->hostname;
- } elseif (isset($_SERVER, $_SERVER['SERVER_NAME'])) {
- return $_SERVER['SERVER_NAME'];
- }
-
- return ($host = \gethostname()) === false ? 'localhost.localdomain' : $host;
- }
-
- public function addHeader(string $name, string $value = nulll) : bool
- {
- if ($vallue === null && \strpos($name, ':') !== false) {
- list($name, $value) = \explode(':', $name, 2);
- }
-
- $name = \trim($name);
- $value = \trim($value);
-
- if (empty($name) || \strbrk($name . $value, "\r\n") !== false) {
- return false;
- }
-
- // todo: consider to add by name and make the name an array -> multiple values per name
- $this->header[] = [$name, $value];
-
- return true;
- }
-
- public function getHeader() : array
- {
- return $this->header;
- }
-
- public function msgHtml(string $message, string $baseDir = '') : string
- {
- }
-
- /**
- * Set signing files
- *
- * @param string $certFile Certification file path
- * @param string $keyFile Key file path
- * @param string $keyPass Password for the key
- *
- * @return void
- *
- * @since 1.0.0
- */
- public function sign(string $certFile, string $keyFile, string $keyPass) : void
- {
- $this->signCertFile = $certFile;
- $this->signKeyFile = $keyFile;
- $this->signKeyPass = $keyPass;
- }
-
- public function preSend() : void
- {
- $this->mimeHeader = '';
- $this->mimeBody = $this->createBody();
- // @todo: only if createBody impements sign / #tempheader = $this->header
- $this->mimeHeader = $this->createHeader();
-
- // set mime body
- // set mime header
- // ...
- }
-
- private function addrAppend(string $type, array $addr) : string
- {
- }
-}
diff --git a/Message/Mail/MailBoxInterface.php b/Message/Mail/MailBoxInterface.php
new file mode 100644
index 000000000..b324de7b8
--- /dev/null
+++ b/Message/Mail/MailBoxInterface.php
@@ -0,0 +1,29 @@
+username = $user;
+ $this->password = $pass;
+ $this->port = $port;
+ $this->encryption = $encryption;
+ }
+
+ /**
+ * Destructor.
+ *
+ * @since 1.0.0
+ */
+ public function __destruct()
+ {
+ $this->smtpClose();
+ }
+
+ /**
+ * Set the mailer and the mailer tool
+ *
+ * @param string $mailer Mailer
+ *
+ * @return void
+ *
+ * @since 1.0.0
+ */
+ public function setMailer(string $mailer) : void
+ {
+ $this->mailer = $mailer;
+
+ switch ($mailer) {
+ case SubmitType::MAIL:
+ case SubmitType::SMTP:
+ return;
+ case SubmitType::SENDMAIL:
+ $this->mailerTool = \stripos($sendmailPath = \ini_get('sendmail_path'), 'sendmail') === false
+ ? '/usr/sbin/sendmail'
+ : $sendmailPath;
+ return;
+ case SubmitType::QMAIL:
+ $this->mailerTool = \stripos($sendmailPath = \ini_get('sendmail_path'), 'qmail') === false
+ ? '/var/qmail/bin/qmail-inject'
+ : $sendmailPath;
+ return;
+ default:
+ return;
+ }
+ }
+
+ /**
+ * Send mail
+ *
+ * @param $mail Mail
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function send(Email $mail) : bool
+ {
+ if (!$mail->preSend($this->mailer)) {
+ return false;
+ }
+
+ return $this->postSend($mail);
+ }
+
+ /**
+ * Send the mail
+ *
+ * @param Email $mail Mail
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ private function postSend(Email $mail) : bool
+ {
+ switch ($this->mailer) {
+ case SubmitType::SENDMAIL:
+ case SubmitType::QMAIL:
+ return $this->sendmailSend($mail);
+ case SubmitType::SMTP:
+ return $this->smtpSend($mail);
+ case SubmitType::MAIL:
+ return $this->mailSend($mail);
+ default:
+ return false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Send mail
+ *
+ * @param Email $mail Mail
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ protected function sendmailSend(Email $mail) : bool
+ {
+ $header = \rtrim($mail->headerMime, " \r\n\t") . self::$LE . self::$LE;
+
+ // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
+ if (!empty($mail->sender) && StringUtils::isShellSafe($mail->sender)) {
+ $mailerToolFmt = $this->mailer === SubmitType::QMAIL
+ ? '%s -f%s'
+ : '%s -oi -f%s -t';
+ } elseif ($this->mailer === SubmitType::QMAIL) {
+ $mailerToolFmt = '%s';
+ } else {
+ $mailerToolFmt = '%s -oi -t';
+ }
+
+ $mailerTool = \sprintf($mailerToolFmt, \escapeshellcmd($this->sendmail), $mail->sender);
+
+ $con = \popen($mailerTool, 'w');
+ if ($con === false) {
+ return false;
+ }
+
+ \fwrite($con, $header);
+ \fwrite($con, $mail->bodyMime);
+
+ $result = \pclose($con);
+ if ($result !== 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Send mail
+ *
+ * @param Email $mail Mail
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ protected function mailSend(Email $mail) : bool
+ {
+ $header = \rtrim($mail->headerMime, " \r\n\t") . self::$LE . self::$LE;
+
+ $toArr = [];
+ foreach ($mail->to as $toaddr) {
+ $toArr[] = $mail->addrFormat($toaddr);
+ }
+
+ $to = \implode(', ', $toArr);
+
+ //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
+ // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
+ $params = null;
+ if (!empty($mail->sender) && EmailValidator::isValid($mail->sender) && StringUtils::isShellSafe($mail->sender)) {
+ $params = \sprintf('-f%s', $mail->sender);
+ }
+
+ if (!empty($mail->sender) && EmailValidator::isValid($mail->sender)) {
+ $oldFrom = \ini_get('sendmail_from');
+ \ini_set('sendmail_from', $mail->sender);
+ }
+
+ $result = false;
+ $result = $this->mailPassthru($to, $mail, $header, $params);
+
+ if (isset($oldFrom)) {
+ \ini_set('sendmail_from', $oldFrom);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Call mail() in a safe_mode-aware fashion.
+ *
+ * @param string $to To
+ * @param Email $mail Mail
+ * @param string $body Message Body
+ * @param string $header Additional Header(s)
+ * @param null|string $params Params
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ private function mailPassthru(string $to, Email $mail, string $header, string $params = null) : bool
+ {
+ $subject = $mail->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $mail->subject)));
+
+ return !$this->useMailOptions || $params === null
+ ? \mail($to, $subject, $mail->body, $header)
+ : \mail($to, $subject, $mail->body, $header, $params);
+ }
+
+ /**
+ * Send mail
+ *
+ * @param Email $mail Mail
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ protected function smtpSend(Email $mail) : bool
+ {
+ $header = \rtrim($mail->headerMime, " \r\n\t") . self::$LE . self::$LE;
+
+ if (!$this->smtpConnect($this->smtpOptions)) {
+ return false;
+ }
+
+ $smtpFrom = $mail->sender === '' ? $mail->from : $mail->sender;
+
+ if (!$this->smtp->mail($smtpFrom)) {
+ return false;
+ }
+
+ $badRcpt = [];
+ foreach ([$mail->to, $mail->cc, $mail->bcc] as $togroup) {
+ foreach ($togroup as $to) {
+ $badRcpt = $this->smtp->recipient($to[0], $this->dsn) ? $badRcpt + 1 : $badRcpt;
+ }
+ }
+
+ // Only send the DATA command if we have viable recipients
+ if ((\count($this->to) + \count($this->cc) + \count($this->bcc) > $badRcpt)
+ && !$this->smtp->data($header . $mail->body, self::MAX_LINE_LENGTH)
+ ) {
+ return false;
+ }
+
+ //$transactinoId = $this->smtp->getLastTransactionId();
+
+ if ($this->keepAlive) {
+ $this->smtp->reset();
+ } else {
+ $this->smtp->quit();
+ $this->smtp->close();
+ }
+
+ if (!empty($badRcpt)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Initiate a connection to an SMTP server.
+ *
+ * @param array $options An array of options compatible with stream_context_create()
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function smtpConnect(array $options = null) : bool
+ {
+ if ($this->smtp === null) {
+ $this->smtp = new Smtp();
+ }
+
+ if ($this->smtp->isConnected()) {
+ return true;
+ }
+
+ if ($options === null) {
+ $options = $this->smtpOptions;
+ }
+
+ $this->smtp->timeout = $this->timeout;
+ $this->smtp->doVerp = $this->useVerp;
+
+ $hosts = \explode(';', $this->host);
+ foreach ($hosts as $hostentry) {
+ $hostinfo = [];
+ if (!\preg_match(
+ '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
+ \trim($hostentry),
+ $hostinfo
+ )
+ ) {
+ // Not a valid host entry
+ continue;
+ }
+
+ // $hostinfo[1]: optional ssl or tls prefix
+ // $hostinfo[2]: the hostname
+ // $hostinfo[3]: optional port number
+
+ //Check the host name is a valid name or IP address
+ if (!Hostname::isValid($hostinfo[2])) {
+ continue;
+ }
+
+ $prefix = '';
+ $secure = $this->encryption;
+ $tls = ($this->encryption === EncryptionType::TLS);
+
+ if ($hostinfo[1] === 'ssl' || ($hostinfo[1] === '' && $this->encryption === EncryptionType::SMTPS)) {
+ $prefix = 'ssl://';
+ $tls = false;
+ $secure = EncryptionType::SMTPS;
+ } elseif ('tls' === $hostinfo[1]) {
+ $tls = true;
+ $secure = EncryptionType::TLS;
+ }
+
+ //Do we need the OpenSSL extension?
+ $sslExt = defined('OPENSSL_ALGO_SHA256');
+ if (($secure === EncryptionType::TLS || $secure === EncryptionType::SMTPS)
+ && !$sslExt
+ ) {
+ return false;
+ }
+
+ $host = $hostinfo[2];
+ $port = $this->port;
+
+ if (isset($hostinfo[3])
+ && \is_numeric($hostinfo[3])
+ && $hostinfo[3] > 0 && $hostinfo[3] < 65536
+ ) {
+ $port = (int) $hostinfo[3];
+ }
+
+ if ($this->smtp->connect($prefix . $host, $port, $this->timeout, $options)) {
+ $hello = !empty($this->helo) ? $this->helo : SystemUtils::getHostname();
+
+ $this->smtp->hello($hello);
+ $tls = $this->useAutoTLS && $sslExt && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')
+ ? true : $tls;
+
+ $this->smtp->hello($hello);
+
+ //Automatically enable TLS encryption
+ $tls = $this->useAutoTLS && $sslExt && $secure !== EncryptionType::SMTPS && $this->smtp->getServerExt('STARTTLS')
+ ? true : $tls;
+
+ if ($tls) {
+ if (!$this->smtp->startTLS()) {
+ return false;
+ }
+
+ // Resend EHLO
+ $this->smtp->hello($hello);
+ }
+
+ return !($this->useSMTPAuth
+ && !$this->smtp->authenticate($this->username, $this->password, $this->authType, $this->oauth )
+ );
+ }
+ }
+
+ // If we get here, all connection attempts have failed
+ $this->smtp->close();
+
+ return false;
+ }
+
+ /**
+ * Close SMTP
+ *
+ * @return void
+ *
+ * @since 1.0.0
+ */
+ public function smtpClose() : void
+ {
+ if ($this->smtp !== null && $this->smtp->isConnected()) {
+ $this->smtp->quit();
+ $this->smtp->close();
+ }
+ }
+}
diff --git a/Message/Mail/MessageInterface.php b/Message/Mail/MessageInterface.php
new file mode 100644
index 000000000..152b12247
--- /dev/null
+++ b/Message/Mail/MessageInterface.php
@@ -0,0 +1,40 @@
+inboxClose();
+ parent::__destruct();
+ }
+
+ /**
+ * Connect to server
*
* @return bool
*
* @since 1.0.0
*/
- public function connect(string $user = '', string $pass = '') : bool
+ public function connectInbox() : bool
{
- $this->mailbox = '{' . $this->host . ':' . $this->port . '/pop3}';
+ $this->mailbox = ($tmp = \imap_open(
+ '{'
+ . $this->host . ':' . $this->port . '/pop3'
+ . ($this->encryption !== EncryptionType::NONE ? '/ssl' : '')
+ . '}',
+ $this->username, $this->password
+ ) === false) ? null : $tmp;
+ }
- return parent::connect();
+ public function getBoxes() : array
+ {
+ $list = \imap_list($this->mailbox, '{' . $this->host . ':' . $this->port . '}');
+ if (!\is_array($list)) {
+ return [];
+ }
+
+ foreach ($list as $key => $value) {
+ $list[$key] = \imap_utf7_decode($value);
+ }
+
+ return $list;
+ }
+
+ public function renameBox(string $old, string $new) : bool
+ {
+ return \imap_renamemailbox($this->mailbox, $old, \imap_utf7_encode($new));
+ }
+
+ public function deleteBox(string $box) : bool
+ {
+ return \imap_deletemailbox($this->mailbox, $box);
+ }
+
+ public function createBox(string $box) : bool
+ {
+ return \imap_createmailbox($this->mailbox, \imap_utf7_encode($box));
+ }
+
+ public function countMail(string $box) : int
+ {
+ if ($this->box !== $box) {
+ \imap_reopen($this->box, $box);
+ $this->box = $box;
+ }
+
+ return \imap_num_msg($this->mailbox);
+ }
+
+ public function getMailboxInfo(string $box) : object
+ {
+ if ($this->box !== $box) {
+ \imap_reopen($this->box, $box);
+ $this->box = $box;
+ }
+
+ return \imap_status($this->mailbox);
+ }
+
+ public function getRecentCount(string $box) : int
+ {
+ if ($this->box !== $box) {
+ \imap_reopen($this->box, $box);
+ $this->box = $box;
+ }
+
+ return \imap_num_recent($this->mailbox);
+ }
+
+ public function copyMail(string|array $messages, string $box) : bool
+ {
+ return \imap_mail_copy($this->mailbox, !\is_string($messages) ? \implode(',', $messages) : $messages, $box);
+ }
+
+ public function moveMail(string|array $messages, string $box) : bool
+ {
+ return \imap_mail_copy($this->mailbox, !\is_string($messages) ? \implode(',', $messages) : $messages, $box);
+ }
+
+ public function deleteMail(int $msg) : bool
+ {
+ return \imap_delete($this->mailbox, $msg);
+ }
+
+ public function getHeaders(string $box) : array
+ {
+ if ($this->box !== $box) {
+ \imap_reopen($this->box, $box);
+ $this->box = $box;
+ }
+
+ return \imap_headers($this->mailbox);
+ }
+
+ public function getHeaderInfo(int $msg) : object
+ {
+ return \imap_headerinfo($this->mailbox, $msg);
+ }
+
+ public function getMail(int $msg) : Email
+ {
+ return new Email();
}
/**
- * {@inheritdoc}
+ * Close mailbox
+ *
+ * @return void
+ *
+ * @since 1.0.0
*/
- public function send() : bool
+ public function inboxClose() : void
{
+ if ($this->mailbox !== null) {
+ \imap_close($this->mailbox);
+ }
}
}
diff --git a/Message/Mail/SMTPAuthType.php b/Message/Mail/SMTPAuthType.php
new file mode 100644
index 000000000..c56e2c2fc
--- /dev/null
+++ b/Message/Mail/SMTPAuthType.php
@@ -0,0 +1,38 @@
+isConnected()) {
+ return false;
+ }
+
+ $this->con = $this->getSMTPConnection($host, $port, $timeout, $options);
+ if ($this->con === null) {
+ return false;
+ }
+
+ $this->lastReply = $this->getLines();
+ $responseCode = (int) \substr($this->lastReply, 0, 3);
+ if ($responseCode === 220) {
+ return true;
+ }
+
+ if ($responseCode === 554) {
+ $this->quit();
+ }
+
+ $this->close();
+
+ return false;
+ }
+
+ /**
+ * Create connection to the SMTP server.
+ *
+ * @param string $host SMTP server IP or host name
+ * @param int $port The port number to connect to
+ * @param int $timeout How long to wait for the connection to open
+ * @param array $options An array of options for stream_context_create()
+ *
+ * @return null|resource
+ *
+ * @since 1.0.0
+ */
+ protected function getSMTPConnection(string $host, int $port = 25, int $timeout = 30, array $options = []) : mixed
+ {
+ static $streamok;
+ if ($streamok === null) {
+ $streamok = \function_exists('stream_socket_client');
+ }
+
+ $errno = 0;
+ $errstr = '';
+
+ if ($streamok) {
+ $socketContext = \stream_context_create($options);
+ $connection = \stream_socket_client($host . ':' . $port, $errno, $errstr, $timeout, \STREAM_CLIENT_CONNECT, $socketContext);
+ } else {
+ //Fall back to fsockopen which should work in more places, but is missing some features
+ $connection = \fsockopen($host, $port, $errno, $errstr, $timeout);
+ }
+
+ if (!\is_resource($connection)) {
+ return null;
+ }
+
+ // SMTP server can take longer to respond, give longer timeout for first read
+ // Windows does not have support for this timeout function
+ if (\strpos(\PHP_OS, 'WIN') !== 0) {
+ $max = (int) \ini_get('max_execution_time');
+ if ($max !== 0 && $timeout > $max && \strpos(\ini_get('disable_functions'), 'set_time_limit') === false) {
+ \set_time_limit($timeout);
+ }
+
+ \stream_set_timeout($connection, $timeout, 0);
+ }
+
+ return $connection === false ? null : $connection;
+ }
+
+ /**
+ * Initiate a TLS (encrypted) session.
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function startTLS() : bool
+ {
+ if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
+ return false;
+ }
+
+ $crypto_method = \STREAM_CRYPTO_METHOD_TLS_CLIENT;
+ if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
+ $crypto_method |= \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
+ $crypto_method |= \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
+ }
+
+ return (bool) \stream_socket_enable_crypto($this->con, true, $crypto_method);
+ }
+
+ /**
+ * Perform SMTP authentication.
+ * Must be run after hello().
+ *
+ * @param string $username The user name
+ * @param string $password The password
+ * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
+ * @param OAuth $oauth An optional OAuth instance for XOAUTH2 authentication
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function authenticate(
+ string $username,
+ string $password,
+ string $authtype = '',
+ mixed $oauth = null
+ ) : bool {
+ if (empty($this->serverCaps)) {
+ return false;
+ }
+
+ if (isset($this->serverCaps['EHLO'])) {
+ // SMTP extensions are available; try to find a proper authentication method
+ if (!isset($this->serverCaps['AUTH'])) {
+ // 'at this stage' means that auth may be allowed after the stage changes
+ // e.g. after STARTTLS
+ return false;
+ }
+
+ //If we have requested a specific auth type, check the server supports it before trying others
+ if ($authtype !== '' && !\in_array($authtype, $this->serverCaps['AUTH'], true)) {
+ $authtype = '';
+ }
+
+ if ($authtype !== '') {
+ //If no auth mechanism is specified, attempt to use these, in this order
+ //Try CRAM-MD5 first as it's more secure than the others
+ foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
+ if (\in_array($method, $this->serverCaps['AUTH'], true)) {
+ $authtype = $method;
+ break;
+ }
+ }
+
+ if ($authtype === '') {
+ return false;
+ }
+ }
+
+ if (!\in_array($authtype, $this->serverCaps['AUTH'], true)) {
+ return false;
+ }
+ } elseif ($authtype === '') {
+ $authtype = 'LOGIN';
+ }
+
+ switch ($authtype) {
+ case 'PLAIN':
+ // Start authentication
+ if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)
+ || !$this->sendCommand('User & Password',
+ \base64_encode("\0" . $username . "\0" . $password),
+ 235
+ )
+ ) {
+ return false;
+ }
+ break;
+ case 'LOGIN':
+ // Start authentication
+ if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)
+ || !$this->sendCommand('Username', \base64_encode($username), 334)
+ || !$this->sendCommand('Password', \base64_encode($password), 235)
+ ) {
+ return false;
+ }
+ break;
+ case 'CRAM-MD5':
+ // Start authentication
+ if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
+ return false;
+ }
+
+ $challenge = \base64_decode(\substr($this->lastReply, 4));
+ $response = $username . ' ' . $this->hmac($challenge, $password);
+
+ // send encoded credentials
+ return $this->sendCommand('Username', \base64_encode($response), 235);
+ case 'XOAUTH2':
+ //The OAuth instance must be set up prior to requesting auth.
+ if ($OAuth === null) {
+ return false;
+ }
+
+ $oauth = $OAuth->getOauth64();
+ if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
+ return false;
+ }
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Calculate an MD5 HMAC hash.
+ *
+ * @param string $data The data to hash
+ * @param string $key The key to hash with
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ protected function hmac(string $data, string $key) : string
+ {
+ // RFC 2104 HMAC implementation for php.
+ // Creates an md5 HMAC.
+ // by Lance Rushing
+ $byteLen = 64;
+ if (strlen($key) > $byteLen) {
+ $key = \pack('H*', \md5($key));
+ }
+
+ $key = \str_pad($key, $byteLen, \chr(0x00));
+ $ipad = \str_pad('', $byteLen, \chr(0x36));
+ $opad = \str_pad('', $byteLen, \chr(0x5c));
+ $k_ipad = $key ^ $ipad;
+ $k_opad = $key ^ $opad;
+
+ return \md5($k_opad . \pack('H*', \md5($k_ipad . $data)));
+ }
+
+ /**
+ * Check connection state.
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function isConnected() : bool
+ {
+ if (!\is_resource($this->con)) {
+ return false;
+ }
+
+ $status = \stream_get_meta_data($this->con);
+ if ($status['eof']) {
+ $this->close();
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Close the socket and clean up the state of the class.
+ * Try to QUIT first!
+ *
+ * @return void
+ *
+ * @since 1.0.0
+ */
+ public function close() : void
+ {
+ $this->serverCaps = [];
+ $this->heloRply = '';
+
+ if (\is_resource($this->con)) {
+ fclose($this->con);
+ $this->con = null;
+ }
+ }
+
+ /**
+ * Send an SMTP DATA command.
+ *
+ * @param string $msg_data Message data to send
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function data($msg_data, int $maxLineLength = 998) : bool
+ {
+ if (!$this->sendCommand('DATA', 'DATA', 354)) {
+ return false;
+ }
+
+ /* The server is ready to accept data!
+ * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
+ */
+ $lines = \explode("\n", \str_replace(["\r\n", "\r"], "\n", $msg_data));
+
+ /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
+ * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
+ * process all lines before a blank line as headers.
+ */
+ $field = \substr($lines[0], 0, \strpos($lines[0], ':'));
+ $inHeaders = (!empty($field) && \strpos($field, ' ') === false);
+
+ foreach ($lines as $line) {
+ $linesOut = [];
+ if ($inHeaders && $line === '') {
+ $inHeaders = false;
+ }
+
+ while (isset($line[$maxLineLength])) {
+ $pos = \strrpos(\substr($line, 0, $maxLineLength), ' ');
+ if (!$pos) {
+ $pos = $maxLineLength - 1;
+ $linesOut[] = \substr($line, 0, $pos);
+ $line = \substr($line, $pos);
+ } else {
+ $linesOut[] = \substr($line, 0, $pos);
+ $line = \substr($line, $pos + 1);
+ }
+
+ if ($inHeaders) {
+ $line = "\t" . $line;
+ }
+ }
+
+ $linesOut[] = $line;
+
+ foreach ($linesOut as $lineOut) {
+ if (!empty($lineOut) && $lineOut[0] === '.') {
+ $lineOut = '.' . $lineOut;
+ }
+
+ $this->clientSend($lineOut . self::$LE, 'DATA');
+ }
+ }
+
+ $tmpTimeLimit = $this->timeLimit;
+ $this->timeLimit *= 2;
+ $result = $this->sendCommand('DATA END', '.', 250);
+
+ $this->recordLastTransactionId();
+
+ $this->timeLimit = $tmpTimeLimit;
+
+ return $result;
+ }
+
+ /**
+ * Send an SMTP HELO or EHLO command.
+ *
+ * @param string $host The host name or IP to connect to
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function hello(string $host = '') : bool
+ {
+ if ($this->sendHello('EHLO', $host)) {
+ return true;
+ }
+
+ if (\substr($this->heloRply, 0, 3) == '421') {
+ return false;
+ }
+
+ return $this->sendHello('HELO', $host);
+ }
+
+ /**
+ * Send an SMTP HELO or EHLO command.
+ *
+ * @param string $hello The HELO string
+ * @param string $host The hostname to say we are
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ protected function sendHello(string $hello, string $host) : bool
+ {
+ $status = $this->sendCommand($hello, $hello . ' ' . $host, 250);
+ $this->heloRply = $this->lastReply;
+
+ if ($status) {
+ $this->parseHelloFields($hello);
+ } else {
+ $this->serverCaps = [];
+ }
+
+ return $status;
+ }
+
+ /**
+ * Parse a reply to HELO/EHLO command to discover server extensions.
+ *
+ * @param string $type `HELO` or `EHLO`
+ *
+ * @return void
+ *
+ * @since 1.0.0
+ */
+ protected function parseHelloFields(string $type) : void
+ {
+ $this->serverCaps = [];
+ $lines = \explode("\n", $this->heloRply);
+
+ foreach ($lines as $n => $s) {
+ //First 4 chars contain response code followed by - or space
+ $s = \trim(\substr($s, 4));
+ if (empty($s)) {
+ continue;
+ }
+
+ $fields = \explode(' ', $s);
+ if (!empty($fields)) {
+ if (!$n) {
+ $name = $type;
+ $fields = $fields[0];
+ } else {
+ $name = \array_shift($fields);
+ switch ($name) {
+ case 'SIZE':
+ $fields = ($fields ? $fields[0] : 0);
+ break;
+ case 'AUTH':
+ if (!\is_array($fields)) {
+ $fields = [];
+ }
+ break;
+ default:
+ $fields = true;
+ }
+ }
+
+ $this->serverCaps[$name] = $fields;
+ }
+ }
+ }
+
+ /**
+ * Send an SMTP MAIL command.
+ *
+ * @param string $from Source address of this message
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function mail(string $from) : bool
+ {
+ $useVerp = ($this->doVerp ? ' XVERP' : '');
+
+ return $this->sendCommand('MAIL FROM', 'MAIL FROM:<' . $from . '>' . $useVerp, 250);
+ }
+
+ /**
+ * Send an SMTP QUIT command.
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function quit() : bool
+ {
+ $status = $this->sendCommand('QUIT', 'QUIT', 221);
+ if ($status) {
+ $this->close();
+ }
+
+ return $status;
+ }
+
+ /**
+ * Send an SMTP RCPT command.
+ *
+ * @param string $address The address the message is being sent to
+ * @param string $dsn Comma separated list of DSN notifications
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function recipient(string $address, string $dsn = DsnNotificationType::NONE) : bool
+ {
+ if ($dsn === '') {
+ $rcpt = 'RCPT TO:<' . $address . '>';
+ } else {
+ $notify = [];
+
+ if (\strpos($dsn, 'NEVER') !== false) {
+ $notify[] = 'NEVER';
+ } else {
+ foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
+ if (\strpos($dsn, $value) !== false) {
+ $notify[] = $value;
+ }
+ }
+ }
+
+ $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . \implode(',', $notify);
+ }
+
+ return $this->sendCommand('RCPT TO', $rcpt, [250, 251]);
+ }
+
+ /**
+ * Send an SMTP RSET command.
+ *
+ * @return bool s
+ *
+ * @since 1.0.0
+ */
+ public function reset() : bool
+ {
+ return $this->sendCommand('RSET', 'RSET', 250);
+ }
+
+ /**
+ * Send a command to an SMTP server and check its return code.
+ *
+ * @param string $command The command name - not sent to the server
+ * @param string $commandstring The actual command to send
+ * @param int|array $expect One or more expected integer success codes
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ protected function sendCommand(string $command, string $commandstring, int|array $expect) : bool
+ {
+ if (!$this->isConnected()) {
+ return false;
+ }
+
+ if ((\strpos($commandstring, "\n") !== false)
+ || (\strpos($commandstring, "\r") !== false)
+ ) {
+ return false;
+ }
+
+ $this->clientSend($commandstring . self::$LE, $command);
+
+ $this->lastReply = $this->getLines();
+
+ $matches = [];
+ if (\preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->lastReply, $matches)) {
+ $code = (int) $matches[1];
+ $codeEx = \count($matches) > 2 ? $matches[2] : null;
+
+ // Cut off error code from each response line
+ $detail = \preg_replace(
+ "/{$code}[ -]" .
+ ($codeEx ? \str_replace('.', '\\.', $codeEx) . ' ' : '') . '/m',
+ '',
+ $this->lastReply
+ );
+ } else {
+ // Fall back to simple parsing if regex fails
+ $code = (int) \substr($this->lastReply, 0, 3);
+ $detail = \substr($this->lastReply, 4);
+ }
+
+ if (!\in_array($code, (array) $expect, true)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Send an SMTP SAML command.
+ * Starts a mail transaction from the email address specified in $from.
+ *
+ * @param string $from The address the message is from
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function sendAndMail(string $from) : bool
+ {
+ return $this->sendCommand('SAML', "SAML FROM:$from", 250);
+ }
+
+ /**
+ * Send an SMTP VRFY command.
+ *
+ * @param string $name The name to verify
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function verify(string $name) : bool
+ {
+ return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
+ }
+
+ /**
+ * Send an SMTP NOOP command.
+ * Used to keep keep-alives alive.
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function noop() : bool
+ {
+ return $this->sendCommand('NOOP', 'NOOP', 250);
+ }
+
+ /**
+ * Send raw data to the server.
+ *
+ * @param string $data The data to send
+ * @param string $command Optionally, the command this is part of, used only for controlling debug output
+ *
+ * @return int
+ *
+ * @since 1.0.0
+ */
+ public function clientSend(string $data, string $command = '') : int
+ {
+ $result = \fwrite($this->con, $data);
+
+ return $result === false ? -1 : $result;
+ }
+
+ /**
+ * Get SMTP extensions available on the server.
+ *
+ * @return array
+ *
+ * @since 1.0.0
+ */
+ public function getServerExtList() : array
+ {
+ return $this->serverCaps;
+ }
+
+ /**
+ * Get metadata about the SMTP server from its HELO/EHLO response.
+ *
+ * @param string $name Name of SMTP extension
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public function getServerExt(string $name) : bool
+ {
+ if (empty($this->serverCaps)) {
+ // HELO/EHLO has not been sent
+ return false;
+ }
+
+ if (!isset($this->serverCaps[$name])) {
+ if ($name === 'HELO') {
+ // Server name
+ //return $this->serverCaps['EHLO'];
+ }
+
+ return false;
+ }
+
+ return isset($this->serverCaps[$name]);
+ }
+
+ /**
+ * Get the last reply from the server.
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ public function getLastReply() : string
+ {
+ return $this->lastReply;
+ }
+
+ /**
+ * Read the SMTP server's response.
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ protected function getLines() : string
+ {
+ if (!\is_resource($this->con)) {
+ return '';
+ }
+
+ $data = '';
+ $endTime = 0;
+
+ \stream_set_timeout($this->con, $this->timeout);
+ if ($this->timeLimit > 0) {
+ $endTime = \time() + $this->timeLimit;
+ }
+
+ $selR = [$this->con];
+ $selW = null;
+ $tries = 0;
+
+ while (\is_resource($this->con) && !\feof($this->con)) {
+ $n = \stream_select($selR, $selW, $selW, $this->timeLimit);
+ if ($n === false) {
+ if ($tries < 3) {
+ ++$tries;
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ $str = \fgets($this->con, self::MAX_REPLY_LENGTH);
+ $data .= $str;
+
+ // If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
+ // or 4th character is a space or a line break char, we are done reading, break the loop.
+ // String array access is a significant micro-optimisation over strlen
+ if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
+ break;
+ }
+
+ $info = \stream_get_meta_data($this->con);
+ if ($info['timed_out']) {
+ break;
+ }
+
+ // Now check if reads took too long
+ if ($endTime && \time() > $endTime) {
+ break;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Extract and return the ID of the last SMTP transaction
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ protected function recordLastTransactionId() : string
+ {
+ $reply = $this->getLastReply();
+
+ if ($reply === '') {
+ $this->lastSmtpTransactionId = '';
+ } else {
+ $this->lastSmtpTransactionId = '';
+ $patterns = SmtpTransactionPattern::getConstants();
+
+ foreach ($patterns as $pattern) {
+ $matches = [];
+ if (\preg_match($pattern, $reply, $matches)) {
+ $this->lastSmtpTransactionId = \trim($matches[1]);
+ break;
+ }
+ }
+ }
+
+ return $this->lastSmtpTransactionId;
+ }
+
+ /**
+ * Get the queue/transaction ID of the last SMTP transaction
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ public function getLastTransactionId() : string
+ {
+ return $this->lastSmtpTransactionId;
+ }
+}
diff --git a/Message/Mail/SmtpTransactionPattern.php b/Message/Mail/SmtpTransactionPattern.php
new file mode 100644
index 000000000..1050488b9
--- /dev/null
+++ b/Message/Mail/SmtpTransactionPattern.php
@@ -0,0 +1,42 @@
+ '', 'basename' => '', 'extension' => '', 'filename' => ''];
+ $pathinfo = [];
+
+ if (\preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
+ if (isset($pathinfo[1])) {
+ $ret['dirname'] = $pathinfo[1];
+ }
+ if (isset($pathinfo[2])) {
+ $ret['basename'] = $pathinfo[2];
+ }
+ if (isset($pathinfo[5])) {
+ $ret['extension'] = $pathinfo[5];
+ }
+ if (isset($pathinfo[3])) {
+ $ret['filename'] = $pathinfo[3];
+ }
+ }
+
+ switch ($options) {
+ case \PATHINFO_DIRNAME:
+ case 'dirname':
+ return $ret['dirname'];
+ case \PATHINFO_BASENAME:
+ case 'basename':
+ return $ret['basename'];
+ case \PATHINFO_EXTENSION:
+ case 'extension':
+ return $ret['extension'];
+ case \PATHINFO_FILENAME:
+ case 'filename':
+ return $ret['filename'];
+ default:
+ return $ret;
+ }
+ }
+
+ /**
+ * Check whether a file path is safe, accessible, and readable.
+ *
+ * @param string $path A relative or absolute path
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public static function isAccessible(string $path) : bool
+ {
+ $readable = \is_file($path);
+ if (\strpos($path, '\\\\') !== 0) {
+ $readable = $readable && \is_readable($path);
+ }
+
+ return self::isPermittedPath($path) && $readable;
+ }
+
+ /**
+ * Check whether a file path is of a permitted type.
+ *
+ * @param string $path Path
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public static function isPermittedPath(string $path) : bool
+ {
+ return !\preg_match('#^[a-z]+://#i', $path);
+ }
}
diff --git a/System/MimeType.php b/System/MimeType.php
index bb37fdd58..4e063ecca 100644
--- a/System/MimeType.php
+++ b/System/MimeType.php
@@ -2009,4 +2009,22 @@ abstract class MimeType extends Enum
public const M_ZMM = 'application/vnd.handheld-entertainment+xml';
public const M_123 = 'application/vnd.lotus-1-2-3';
+
+ /**
+ * Get mime from file extension
+ *
+ * @param string $extension Extension
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ public static function extensionToMime(string $extension) : string
+ {
+ try {
+ return self::getByName('M_' . \strtoupper($extension)) ?? 'application/octet-stream';
+ } catch (\Throwable $t) {
+ return 'application/octet-stream';
+ }
+ }
}
diff --git a/System/SystemUtils.php b/System/SystemUtils.php
index 50fb5e544..3c411bd84 100644
--- a/System/SystemUtils.php
+++ b/System/SystemUtils.php
@@ -126,4 +126,24 @@ final class SystemUtils
return (int) $cpuUsage;
}
+
+ /**
+ * Get the server hostname.
+ *
+ * @return string
+ *
+ * @since 1.0.0
+ */
+ public static function getHostname() : string
+ {
+ if (isset($_SERVER['SERVER_NAME'])) {
+ return $_SERVER['SERVER_NAME'];
+ } elseif (($result = gethostname()) !== false) {
+ return $result;
+ } elseif (\php_uname('n') !== false) {
+ return \php_uname('n');
+ }
+
+ return 'localhost.localdomain';
+ }
}
diff --git a/Utils/StringUtils.php b/Utils/StringUtils.php
index 9f2647da6..c94bce119 100644
--- a/Utils/StringUtils.php
+++ b/Utils/StringUtils.php
@@ -399,4 +399,34 @@ final class StringUtils
return (int) $res;
}
+
+ /**
+ * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
+ *
+ * @param string $string String to check
+ *
+ * @return bool
+ *
+ * @since 1.0.0
+ */
+ public static function isShellSafe(string $string)
+ {
+ if (\escapeshellcmd($string) !== $string
+ || !\in_array(\escapeshellarg($string), ["'$string'", "\"$string\""])
+ ) {
+ return false;
+ }
+
+ $length = \strlen($string);
+
+ for ($i = 0; $i < $length; ++$i) {
+ $c = $string[$i];
+
+ if (!\ctype_alnum($c) && \strpos('@_-.', $c) === false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
diff --git a/Validation/Network/Hostname.php b/Validation/Network/Hostname.php
index c8a282f42..8638452f7 100644
--- a/Validation/Network/Hostname.php
+++ b/Validation/Network/Hostname.php
@@ -41,6 +41,21 @@ abstract class Hostname extends ValidatorAbstract
*/
public static function isValid(mixed $value, array $constraints = null) : bool
{
- return \filter_var(\gethostbyname($value), \FILTER_VALIDATE_IP) !== false;
+ //return \filter_var(\gethostbyname($value), \FILTER_VALIDATE_IP) !== false;
+
+ if (empty($value)
+ || \strlen($value) > 256
+ || !\preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+])$/', $value)
+ ) {
+ return false;
+ } elseif (\strlen($value) > 2 && \substr($value, 0, 1) === '[' && \substr($value, -1, 1) === ']') {
+ return \filter_var(\substr($value, 1, -1), \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) !== false;
+ } elseif (\is_numeric(str_replace('.', '', $value))) {
+ return \filter_var($value, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) !== false;
+ } elseif (\filter_var('http://' . $value, FILTER_VALIDATE_URL) !== false) {
+ return true;
+ }
+
+ return false;
}
}
diff --git a/tests/Message/Mail/ImapTest.php b/tests/Message/Mail/ImapTest.php
deleted file mode 100644
index d4b510f5f..000000000
--- a/tests/Message/Mail/ImapTest.php
+++ /dev/null
@@ -1,67 +0,0 @@
-isConnected());
- self::assertTrue($email->connect(
- $GLOBALS['CONFIG']['mail']['imap']['user'],
- $GLOBALS['CONFIG']['mail']['imap']['password']
- ));
-
- self::assertTrue($email->isConnected());
- self::assertEquals([], $email->getBoxes());
- self::assertEquals([], $email->getQuota());
- self::assertInstanceOf('\phpOMS\Message\Mail\Mail', $email->getEmail('1'));
- self::assertEquals([], $email->getInboxAll());
- self::assertEquals([], $email->getInboxOverview());
- self::assertEquals([], $email->getInboxNew());
- self::assertEquals([], $email->getInboxFrom(''));
- self::assertEquals([], $email->getInboxTo(''));
- self::assertEquals([], $email->getInboxCc(''));
- self::assertEquals([], $email->getInboxBcc(''));
- self::assertEquals([], $email->getInboxAnswered());
- self::assertEquals([], $email->getInboxSubject(''));
- self::assertEquals([], $email->getInboxSince(new \DateTime('now')));
- self::assertEquals([], $email->getInboxUnseen());
- self::assertEquals([], $email->getInboxSeen());
- self::assertEquals([], $email->getInboxDeleted());
- self::assertEquals([], $email->getInboxText(''));
- self::assertEquals([], $email->getMessageOverview(1, 1));
- self::assertEquals(0, $email->countMessages());
- self::assertEquals('', $email->getMessageHeader(1));
- }
-}
diff --git a/tests/Message/Mail/MailHandlerTest.php b/tests/Message/Mail/MailHandlerTest.php
new file mode 100644
index 000000000..6cd884b1a
--- /dev/null
+++ b/tests/Message/Mail/MailHandlerTest.php
@@ -0,0 +1,53 @@
+setMailer(SubmitType::MAIL);
+
+ $mail = new Email();
+ $mail->setFrom('d.eichhorn@orange-management.org', 'Dennis Eichhorn');
+ $mail->addTo('coyle.maguire@googlemail.com', 'Donald Duck');
+ $mail->subject = 'Test email';
+ $mail->body = 'This is some content';
+
+ self::assertTrue($mailer->send($mail));
+ }
+
+ public function testReceiveMailWithImap() : void
+ {
+ $mailer = new Imap();
+ $mailer->connectInbox();
+
+ var_dump($mailer->getBoxes());
+ }
+}
diff --git a/tests/Message/Mail/MailTest.php b/tests/Message/Mail/MailTest.php
index 21bcdeea2..e69de29bb 100644
--- a/tests/Message/Mail/MailTest.php
+++ b/tests/Message/Mail/MailTest.php
@@ -1,26 +0,0 @@
-