diff --git a/app/Config/Email.php b/app/Config/Email.php index d9ca1420f6..41415a2f79 100644 --- a/app/Config/Email.php +++ b/app/Config/Email.php @@ -1,10 +1,11 @@ 'MailHandler', + 'sendmail' => 'SendmailHandler', + 'smtp' => 'SMTPHandler', + ]; + + $handler = '\\CodeIgniter\\Email\\Handlers\\' . ($protocolMap[$config->protocol ?? 'mail'] ); + $transporter = new $handler($config); + $transporter->setLogger(static::logger(true)); + + return $transporter; } /** diff --git a/system/Email/Email.php b/system/Email/Email.php index 2c69e879ae..8a2bf3414a 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -1,4 +1,5 @@ - '4 (Low)', 5 => '5 (Lowest)', ]; + /** - * mbstring.func_overload flag + * Logger instance to record error messages and awarnings. * - * @var boolean + * @var \PSR\Log\LoggerInterface */ - protected static $func_overload; + protected $logger; + + /** + * Email transporter that this email is bound to + * + * @var \CodeIgniter\Email\TransporterInterface + */ + protected $handler; + + /** + * Config settings, used if protocol changed. + * + * @var Config\Email + */ + protected $config = null; + //-------------------------------------------------------------------- + /** * Constructor - Sets Email Preferences * @@ -376,10 +328,25 @@ class Email */ public function __construct($config = null) { + if ($config === null) + { + $config = new \Config\Email(); + $this->config = $config; + } + else if (is_array($config)) + { + $mergedConfig = new \Config\Email(); + foreach ($config as $key => $value) + { + $mergedConfig->$key = $value; + } + $this->config = $mergedConfig; + } $this->initialize($config); - isset(static::$func_overload) || static::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); } + //-------------------------------------------------------------------- + /** * Initialize preferences * @@ -394,11 +361,13 @@ class 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]); @@ -409,11 +378,50 @@ class Email } } } - $this->charset = strtoupper($this->charset); - $this->SMTPAuth = isset($this->SMTPUser[0], $this->SMTPPass[0]); + + $this->charset = strtoupper($this->charset); + $this->handler = Services::transporter($config); + return $this; } + //-------------------------------------------------------------------- + + /** + * Set the email protocol to use. + * If valid, get an appropriate handler for it. + * + * @param string $protocol + * + * @returns Email + */ + public function setProtocol(string $protocol = 'mail') + { + if (! in_array($protocol, $this->protocols)) + { + throw EmailException::forInvalidProtocol($protocol); + } + + $this->protocol = $protocol; + $this->config->protocol = $protocol; // update config too + $this->handler = Services::transporter($this->config, false); + + return $this; + } + + /** + * Get the protocol our handler is using. + * Note: the protocol property here is only the requested one. + * + * @return string + */ + public function getProtocol(): string + { + return $this->handler->getProtocol(); + } + + //-------------------------------------------------------------------- + /** * Initialize the Email Data * @@ -423,24 +431,28 @@ class 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->subject = ''; + $this->body = ''; + $this->finalBody = ''; + $this->headerStr = ''; + $this->replyToFlag = false; + $this->recipients = []; + $this->CCArray = []; + $this->BCCArray = []; + $this->headers = []; + $this->setHeader('Date', $this->setDate()); + if ($clearAttachments !== false) { $this->attachments = []; } + return $this; } + //-------------------------------------------------------------------- + /** * Set FROM * @@ -456,19 +468,16 @@ class Email { $from = $match[1]; } + if ($this->validate) { - $this->validateEmail($this->stringToArray($from)); + $this->validateEmail($from); if ($returnPath) { - $this->validateEmail($this->stringToArray($returnPath)); + $this->validateEmail($returnPath); } } - // Store the plain text values - $this->tmpArchive['fromEmail'] = $from; - $this->tmpArchive['fromName'] = $name; - // prepare the display name if ($name !== '') { @@ -483,991 +492,16 @@ class Email $name = $this->prepQEncoding($name); } } + $this->setHeader('From', $name . ' <' . $from . '>'); isset($returnPath) || $returnPath = $from; $this->setHeader('Return-Path', '<' . $returnPath . '>'); - $this->tmpArchive['returnPath'] = $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 !== '') - { - $this->tmpArchive['replyName'] = $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; - $this->tmpArchive['replyTo'] = $replyto; + //-------------------------------------------------------------------- - return $this; - } - //-------------------------------------------------------------------- - /** - * Set Recipients - * - * @param string|array $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; - } - $this->tmpArchive['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)); - $this->tmpArchive['BCCArray'] = $bcc; - } - return $this; - } - //-------------------------------------------------------------------- - /** - * Set Email Subject - * - * @param string $subject - * - * @return Email - */ - public function setSubject($subject) - { - $this->tmpArchive['subject'] = $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|boolean - */ - 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|boolean - */ - 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|array $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|array $email - * - * @return array - */ - 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('#