diff --git a/app/Config/Email.php b/app/Config/Email.php new file mode 100644 index 000000000000..d580c53a9f79 --- /dev/null +++ b/app/Config/Email.php @@ -0,0 +1,164 @@ +setLogger(static::logger(true)); + return $email; + } + + /** + * The Encryption class provides two-way encryption. * * @param mixed $config * @param boolean $getShared @@ -380,13 +403,11 @@ public static function language(string $locale = null, bool $getShared = true) if ($getShared) { return static::getSharedInstance('language', $locale) - ->setLocale($locale); + ->setLocale($locale); } - $locale = ! empty($locale) - ? $locale - : static::request() - ->getLocale(); + $locale = ! empty($locale) ? $locale : static::request() + ->getLocale(); return new Language($locale); } @@ -582,10 +603,10 @@ public static function request(App $config = null, bool $getShared = true) } return new IncomingRequest( - $config, - new URI(), - 'php://input', - new UserAgent() + $config, + new URI(), + 'php://input', + new UserAgent() ); } @@ -638,7 +659,7 @@ public static function redirectResponse(App $config = null, bool $getShared = tr $response = new RedirectResponse($config); $response->setProtocolVersion(static::request() - ->getProtocolVersion()); + ->getProtocolVersion()); return $response; } @@ -905,5 +926,4 @@ public static function typography(bool $getShared = true) } //-------------------------------------------------------------------- - } diff --git a/system/Email/Email.php b/system/Email/Email.php new file mode 100644 index 000000000000..0c72b9532e4d --- /dev/null +++ b/system/Email/Email.php @@ -0,0 +1,2142 @@ + '1 (Highest)', + 2 => '2 (High)', + 3 => '3 (Normal)', + 4 => '4 (Low)', + 5 => '5 (Lowest)', + ]; + /** + * mbstring.func_overload flag + * + * @var boolean + */ + protected static $func_overload; + /** + * Logger instance to record error messages and awarnings. + * + * @var \PSR\Log\LoggerInterface + */ + protected $logger; + //-------------------------------------------------------------------- + /** + * Constructor - Sets Email Preferences + * + * The constructor can be passed an array of config values + * + * @param array|null $config + */ + public function __construct($config = null) + { + $this->initialize($config); + isset(static::$func_overload) || static::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); + log_message('info', 'Email Class Initialized'); + } + //-------------------------------------------------------------------- + /** + * Initialize preferences + * + * @param array|\Config\Email $config + * + * @return Email + */ + public function initialize($config) + { + $this->clear(); + if ($config instanceof \Config\Email) + { + $config = get_object_vars($config); + } + foreach (get_class_vars(get_class($this)) as $key => $value) + { + if (property_exists($this, $key) && isset($config[$key])) + { + $method = 'set' . ucfirst($key); + if (method_exists($this, $method)) + { + $this->$method($config[$key]); + } + else + { + $this->$key = $config[$key]; + } + } + } + $this->charset = strtoupper($this->charset); + $this->SMTPAuth = isset($this->SMTPUser[0], $this->SMTPPass[0]); + return $this; + } + //-------------------------------------------------------------------- + /** + * Initialize the Email Data + * + * @param boolean $clearAttachments + * + * @return Email + */ + public function clear($clearAttachments = false) + { + $this->subject = ''; + $this->body = ''; + $this->finalBody = ''; + $this->headerStr = ''; + $this->replyToFlag = false; + $this->recipients = []; + $this->CCArray = []; + $this->BCCArray = []; + $this->headers = []; + $this->debugMessage = []; + $this->setHeader('Date', $this->setDate()); + if ($clearAttachments !== false) + { + $this->attachments = []; + } + return $this; + } + //-------------------------------------------------------------------- + /** + * Set FROM + * + * @param string $from + * @param string $name + * @param string|null $returnPath Return-Path + * + * @return Email + */ + public function setFrom($from, $name = '', $returnPath = null) + { + if (preg_match('/\<(.*)\>/', $from, $match)) + { + $from = $match[1]; + } + if ($this->validate) + { + $this->validateEmail($this->stringToArray($from)); + if ($returnPath) + { + $this->validateEmail($this->stringToArray($returnPath)); + } + } + // prepare the display name + if ($name !== '') + { + // only use Q encoding if there are characters that would require it + if (! preg_match('/[\200-\377]/', $name)) + { + // add slashes for non-printing characters, slashes, and double quotes, and surround it in double quotes + $name = '"' . addcslashes($name, "\0..\37\177'\"\\") . '"'; + } + else + { + $name = $this->prepQEncoding($name); + } + } + $this->setHeader('From', $name . ' <' . $from . '>'); + isset($returnPath) || $returnPath = $from; + $this->setHeader('Return-Path', '<' . $returnPath . '>'); + return $this; + } + //-------------------------------------------------------------------- + /** + * Set Reply-to + * + * @param string $replyto + * @param string $name + * + * @return Email + */ + public function setReplyTo($replyto, $name = '') + { + if (preg_match('/\<(.*)\>/', $replyto, $match)) + { + $replyto = $match[1]; + } + if ($this->validate) + { + $this->validateEmail($this->stringToArray($replyto)); + } + if ($name !== '') + { + // only use Q encoding if there are characters that would require it + if (! preg_match('/[\200-\377]/', $name)) + { + // add slashes for non-printing characters, slashes, and double quotes, and surround it in double quotes + $name = '"' . addcslashes($name, "\0..\37\177'\"\\") . '"'; + } + else + { + $name = $this->prepQEncoding($name); + } + } + $this->setHeader('Reply-To', $name . ' <' . $replyto . '>'); + $this->replyToFlag = true; + return $this; + } + //-------------------------------------------------------------------- + /** + * Set Recipients + * + * @param string $to + * + * @return Email + */ + public function setTo($to) + { + $to = $this->stringToArray($to); + $to = $this->cleanEmail($to); + if ($this->validate) + { + $this->validateEmail($to); + } + if ($this->getProtocol() !== 'mail') + { + $this->setHeader('To', implode(', ', $to)); + } + $this->recipients = $to; + return $this; + } + //-------------------------------------------------------------------- + /** + * Set CC + * + * @param string $cc + * + * @return Email + */ + public function setCC($cc) + { + $cc = $this->cleanEmail($this->stringToArray($cc)); + if ($this->validate) + { + $this->validateEmail($cc); + } + $this->setHeader('Cc', implode(', ', $cc)); + if ($this->getProtocol() === 'smtp') + { + $this->CCArray = $cc; + } + return $this; + } + //-------------------------------------------------------------------- + /** + * Set BCC + * + * @param string $bcc + * @param string $limit + * + * @return Email + */ + public function setBCC($bcc, $limit = '') + { + if ($limit !== '' && is_numeric($limit)) + { + $this->BCCBatchMode = true; + $this->BCCBatchSize = $limit; + } + $bcc = $this->cleanEmail($this->stringToArray($bcc)); + if ($this->validate) + { + $this->validateEmail($bcc); + } + if ($this->getProtocol() === 'smtp' || ($this->BCCBatchMode && count($bcc) > $this->BCCBatchSize)) + { + $this->BCCArray = $bcc; + } + else + { + $this->setHeader('Bcc', implode(', ', $bcc)); + } + return $this; + } + //-------------------------------------------------------------------- + /** + * Set Email Subject + * + * @param string $subject + * + * @return Email + */ + public function setSubject($subject) + { + $subject = $this->prepQEncoding($subject); + $this->setHeader('Subject', $subject); + return $this; + } + //-------------------------------------------------------------------- + /** + * Set Body + * + * @param string $body + * + * @return Email + */ + public function setMessage($body) + { + $this->body = rtrim(str_replace("\r", '', $body)); + return $this; + } + //-------------------------------------------------------------------- + /** + * Assign file attachments + * + * @param string $file Can be local path, URL or buffered content + * @param string $disposition 'attachment' + * @param string|null $newname + * @param string $mime + * + * @return Email + */ + public function attach($file, $disposition = '', $newname = null, $mime = '') + { + if ($mime === '') + { + if (strpos($file, '://') === false && ! is_file($file)) + { + $this->setErrorMessage(lang('Email.attachmentMissing', [$file])); + return false; + } + if (! $fp = @fopen($file, 'rb')) + { + $this->setErrorMessage(lang('Email.attachmentUnreadable', [$file])); + return false; + } + $fileContent = stream_get_contents($fp); + $mime = $this->mimeTypes(pathinfo($file, PATHINFO_EXTENSION)); + fclose($fp); + } + else + { + $fileContent = & $file; // buffered file + } + // declare names on their own, to make phpcbf happy + $namesAttached = [ + $file, + $newname, + ]; + $this->attachments[] = [ + 'name' => $namesAttached, + 'disposition' => empty($disposition) ? 'attachment' : $disposition, + // Can also be 'inline' Not sure if it matters + 'type' => $mime, + 'content' => chunk_split(base64_encode($fileContent)), + 'multipart' => 'mixed', + ]; + return $this; + } + //-------------------------------------------------------------------- + /** + * Set and return attachment Content-ID + * + * Useful for attached inline pictures + * + * @param string $filename + * + * @return string + */ + public function setAttachmentCID($filename) + { + for ($i = 0, $c = count($this->attachments); $i < $c; $i ++) + { + if ($this->attachments[$i]['name'][0] === $filename) + { + $this->attachments[$i]['multipart'] = 'related'; + $this->attachments[$i]['cid'] = uniqid(basename($this->attachments[$i]['name'][0]) . '@', true); + return $this->attachments[$i]['cid']; + } + } + return false; + } + //-------------------------------------------------------------------- + /** + * Add a Header Item + * + * @param string $header + * @param string $value + * + * @return Email + */ + public function setHeader($header, $value) + { + $this->headers[$header] = str_replace(["\n", "\r"], '', $value); + return $this; + } + //-------------------------------------------------------------------- + /** + * Convert a String to an Array + * + * @param string $email + * + * @return array + */ + protected function stringToArray($email) + { + if (! is_array($email)) + { + return (strpos($email, ',') !== false) ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) : (array) trim($email); + } + return $email; + } + //-------------------------------------------------------------------- + /** + * Set Multipart Value + * + * @param string $str + * + * @return Email + */ + public function setAltMessage($str) + { + $this->altMessage = (string) $str; + return $this; + } + //-------------------------------------------------------------------- + /** + * Set Mailtype + * + * @param string $type + * + * @return Email + */ + public function setMailType($type = 'text') + { + $this->mailType = ($type === 'html') ? 'html' : 'text'; + return $this; + } + //-------------------------------------------------------------------- + /** + * Set Wordwrap + * + * @param boolean $wordWrap + * + * @return Email + */ + public function setWordWrap($wordWrap = true) + { + $this->wordWrap = (bool) $wordWrap; + return $this; + } + //-------------------------------------------------------------------- + /** + * Set Protocol + * + * @param string $protocol + * + * @return Email + */ + public function setProtocol($protocol = 'mail') + { + $this->protocol = in_array($protocol, $this->protocols, true) ? strtolower($protocol) : 'mail'; + return $this; + } + //-------------------------------------------------------------------- + /** + * Set Priority + * + * @param integer $n + * + * @return Email + */ + public function setPriority($n = 3) + { + $this->priority = preg_match('/^[1-5]$/', $n) ? (int) $n : 3; + return $this; + } + //-------------------------------------------------------------------- + /** + * Set Newline Character + * + * @param string $newline + * + * @return Email + */ + public function setNewline($newline = "\n") + { + $this->newline = in_array($newline, ["\n", "\r\n", "\r"]) ? $newline : "\n"; + return $this; + } + //-------------------------------------------------------------------- + /** + * Set CRLF + * + * @param string $CRLF + * + * @return Email + */ + public function setCRLF($CRLF = "\n") + { + $this->CRLF = ($CRLF !== "\n" && $CRLF !== "\r\n" && $CRLF !== "\r") ? "\n" : $CRLF; + return $this; + } + //-------------------------------------------------------------------- + /** + * Get the Message ID + * + * @return string + */ + protected function getMessageID() + { + $from = str_replace(['>', '<'], '', $this->headers['Return-Path']); + return '<' . uniqid('', true) . strstr($from, '@') . '>'; + } + //-------------------------------------------------------------------- + /** + * Get Mail Protocol + * + * @return string + */ + protected function getProtocol() + { + $this->protocol = strtolower($this->protocol); + in_array($this->protocol, $this->protocols, true) || $this->protocol = 'mail'; + return $this->protocol; + } + //-------------------------------------------------------------------- + /** + * Get Mail Encoding + * + * @return string + */ + protected function getEncoding() + { + in_array($this->encoding, $this->bitDepths) || $this->encoding = '8bit'; + foreach ($this->baseCharsets as $charset) + { + if (strpos($this->charset, $charset) === 0) + { + $this->encoding = '7bit'; + break; + } + } + return $this->encoding; + } + //-------------------------------------------------------------------- + /** + * Get content type (text/html/attachment) + * + * @return string + */ + protected function getContentType() + { + if ($this->mailType === 'html') + { + return empty($this->attachments) ? 'html' : 'html-attach'; + } + elseif ($this->mailType === 'text' && ! empty($this->attachments)) + { + return 'plain-attach'; + } + else + { + return 'plain'; + } + } + //-------------------------------------------------------------------- + /** + * Set RFC 822 Date + * + * @return string + */ + protected function setDate() + { + $timezone = date('Z'); + $operator = ($timezone[0] === '-') ? '-' : '+'; + $timezone = abs($timezone); + $timezone = floor($timezone / 3600) * 100 + ($timezone % 3600) / 60; + return sprintf('%s %s%04d', date('D, j M Y H:i:s'), $operator, $timezone); + } + //-------------------------------------------------------------------- + /** + * Mime message + * + * @return string + */ + protected function getMimeMessage() + { + return 'This is a multi-part message in MIME format.' . $this->newline . 'Your email application may not support this format.'; + } + //-------------------------------------------------------------------- + /** + * Validate Email Address + * + * @param string $email + * + * @return boolean + */ + public function validateEmail($email) + { + if (! is_array($email)) + { + $this->setErrorMessage(lang('Email.mustBeArray')); + return false; + } + foreach ($email as $val) + { + if (! $this->isValidEmail($val)) + { + $this->setErrorMessage(lang('Email.invalidAddress', $val)); + return false; + } + } + return true; + } + //-------------------------------------------------------------------- + /** + * Email Validation + * + * @param string $email + * + * @return boolean + */ + public function isValidEmail($email) + { + if (function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46') && $atpos = strpos($email, '@')) + { + $email = static::substr($email, 0, ++ $atpos) . idn_to_ascii( + static::substr($email, $atpos), 0, INTL_IDNA_VARIANT_UTS46 + ); + } + return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); + } + //-------------------------------------------------------------------- + /** + * Clean Extended Email Address: Joe Smith + * + * @param string $email + * + * @return string + */ + public function cleanEmail($email) + { + if (! is_array($email)) + { + return preg_match('/\<(.*)\>/', $email, $match) ? $match[1] : $email; + } + $cleanEmail = []; + foreach ($email as $addy) + { + $cleanEmail[] = preg_match('/\<(.*)\>/', $addy, $match) ? $match[1] : $addy; + } + return $cleanEmail; + } + //-------------------------------------------------------------------- + /** + * Build alternative plain text message + * + * Provides the raw message for use in plain-text headers of + * HTML-formatted emails. + * If the user hasn't specified his own alternative message + * it creates one by stripping the HTML + * + * @return string + */ + protected function getAltMessage() + { + if (! empty($this->altMessage)) + { + return ($this->wordWrap) ? $this->wordWrap($this->altMessage, 76) : $this->altMessage; + } + $body = preg_match('/\(.*)\<\/body\>/si', $this->body, $match) ? $match[1] : $this->body; + $body = str_replace("\t", '', preg_replace('#