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 { } }