* * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace CodeIgniter\Email; use CodeIgniter\Events\Events; use CodeIgniter\I18n\Time; use Config\Mimes; use ErrorException; /** * CodeIgniter Email Class * * Permits email to be sent using Mail, Sendmail, or SMTP. * * @see \CodeIgniter\Email\EmailTest */ class Email { /** * Properties from the last successful send. * * @var array|null */ public $archive; /** * Properties to be added to the next archive. * * @var array */ protected $tmpArchive = []; /** * @var string */ public $fromEmail; /** * @var string */ public $fromName; /** * Used as the User-Agent and X-Mailer headers' value. * * @var string */ public $userAgent = 'CodeIgniter'; /** * Path to the Sendmail binary. * * @var string */ public $mailPath = '/usr/sbin/sendmail'; /** * Which method to use for sending e-mails. * * @var string 'mail', 'sendmail' or 'smtp' */ public $protocol = 'mail'; /** * STMP Server Hostname * * @var string */ public $SMTPHost = ''; /** * SMTP Username * * @var string */ public $SMTPUser = ''; /** * SMTP Password * * @var string */ public $SMTPPass = ''; /** * SMTP Server port * * @var int */ public $SMTPPort = 25; /** * SMTP connection timeout in seconds * * @var int */ public $SMTPTimeout = 5; /** * SMTP persistent connection * * @var bool */ public $SMTPKeepAlive = false; /** * SMTP Encryption * * @var string '', 'tls' or 'ssl'. 'tls' will issue a STARTTLS command * to the server. 'ssl' means implicit SSL. Connection on port * 465 should set this to ''. */ public $SMTPCrypto = ''; /** * Whether to apply word-wrapping to the message body. * * @var bool */ public $wordWrap = true; /** * Number of characters to wrap at. * * @see Email::$wordWrap * * @var int */ public $wrapChars = 76; /** * Message format. * * @var string 'text' or 'html' */ public $mailType = 'text'; /** * Character set (default: utf-8) * * @var string */ public $charset = 'utf-8'; /** * Alternative message (for HTML messages only) * * @var string */ public $altMessage = ''; /** * Whether to validate e-mail addresses. * * @var bool */ public $validate = true; /** * X-Priority header value. * * @var int 1-5 */ public $priority = 3; /** * Newline character sequence. * Use "\r\n" to comply with RFC 822. * * @see http://www.ietf.org/rfc/rfc822.txt * * @var string "\r\n" or "\n" */ public $newline = "\n"; /** * CRLF character sequence * * RFC 2045 specifies that for 'quoted-printable' encoding, * "\r\n" must be used. However, it appears that some servers * (even on the receiving end) don't handle it properly and * switching to "\n", while improper, is the only solution * that seems to work for all environments. * * @see http://www.ietf.org/rfc/rfc822.txt * * @var string */ public $CRLF = "\n"; /** * Whether to use Delivery Status Notification. * * @var bool */ public $DSN = false; /** * Whether to send multipart alternatives. * Yahoo! doesn't seem to like these. * * @var bool */ public $sendMultipart = true; /** * Whether to send messages to BCC recipients in batches. * * @var bool */ public $BCCBatchMode = false; /** * BCC Batch max number size. * * @see Email::$BCCBatchMode * * @var int|string */ public $BCCBatchSize = 200; /** * Subject header * * @var string */ protected $subject = ''; /** * Message body * * @var string */ protected $body = ''; /** * Final message body to be sent. * * @var string */ protected $finalBody = ''; /** * Final headers to send * * @var string */ protected $headerStr = ''; /** * SMTP Connection socket placeholder * * @var resource|null */ protected $SMTPConnect; /** * Mail encoding * * @var string '8bit' or '7bit' */ protected $encoding = '8bit'; /** * Whether to perform SMTP authentication * * @var bool */ protected $SMTPAuth = false; /** * Whether to send a Reply-To header * * @var bool */ protected $replyToFlag = false; /** * Debug messages * * @see Email::printDebugger() * * @var array */ protected $debugMessage = []; /** * Raw debug messages * * @var string[] */ private array $debugMessageRaw = []; /** * Recipients * * @var array|string */ protected $recipients = []; /** * CC Recipients * * @var array */ protected $CCArray = []; /** * BCC Recipients * * @var array */ protected $BCCArray = []; /** * Message headers * * @var array */ protected $headers = []; /** * Attachment data * * @var array */ protected $attachments = []; /** * Valid $protocol values * * @see Email::$protocol * * @var array */ protected $protocols = [ 'mail', 'sendmail', 'smtp', ]; /** * Character sets valid for 7-bit encoding, * excluding language suffix. * * @var list */ protected $baseCharsets = [ 'us-ascii', 'iso-2022-', ]; /** * Bit depths * * Valid mail encodings * * @see Email::$encoding * * @var array */ protected $bitDepths = [ '7bit', '8bit', ]; /** * $priority translations * * Actual values to send with the X-Priority header * * @var array */ protected $priorities = [ 1 => '1 (Highest)', 2 => '2 (High)', 3 => '3 (Normal)', 4 => '4 (Low)', 5 => '5 (Lowest)', ]; /** * mbstring.func_overload flag * * @var bool */ protected static $func_overload; /** * @param array|\Config\Email|null $config */ public function __construct($config = null) { $this->initialize($config); if (! isset(static::$func_overload)) { static::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); } } /** * Initialize preferences * * @param array|\Config\Email|null $config * * @return Email */ public function initialize($config) { $this->clear(); if ($config instanceof \Config\Email) { $config = get_object_vars($config); } foreach (array_keys(get_class_vars(static::class)) as $key) { 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; } /** * @param bool $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->debugMessageRaw = []; $this->setHeader('Date', $this->setDate()); if ($clearAttachments !== false) { $this->attachments = []; } return $this; } /** * @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)); } } $this->tmpArchive['fromEmail'] = $from; $this->tmpArchive['fromName'] = $name; if ($name !== '') { // only use Q encoding if there are characters that would require it if (! preg_match('/[\200-\377]/', $name)) { $name = '"' . addcslashes($name, "\0..\37\177'\"\\") . '"'; } else { $name = $this->prepQEncoding($name); } } $this->setHeader('From', $name . ' <' . $from . '>'); if (! isset($returnPath)) { $returnPath = $from; } $this->setHeader('Return-Path', '<' . $returnPath . '>'); $this->tmpArchive['returnPath'] = $returnPath; return $this; } /** * @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)) { $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; } /** * @param array|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; } /** * @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; } /** * @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; } /** * @param string $subject * * @return Email */ public function setSubject($subject) { $this->tmpArchive['subject'] = $subject; $subject = $this->prepQEncoding($subject); $this->setHeader('Subject', $subject); return $this; } /** * @param string $body * * @return Email */ public function setMessage($body) { $this->body = rtrim(str_replace("\r", '', $body)); return $this; } /** * @param string $file Can be local path, URL or buffered content * @param string $disposition 'attachment' * @param string|null $newname * @param string $mime * * @return bool|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 bool|string */ public function setAttachmentCID($filename) { foreach ($this->attachments as $i => $attachment) { // For file path. if ($attachment['name'][0] === $filename) { $this->attachments[$i]['multipart'] = 'related'; $this->attachments[$i]['cid'] = uniqid(basename($attachment['name'][0]) . '@', true); return $this->attachments[$i]['cid']; } // For buffer string. if ($attachment['name'][1] === $filename) { $this->attachments[$i]['multipart'] = 'related'; $this->attachments[$i]['cid'] = uniqid(basename($attachment['name'][1]) . '@', true); return $this->attachments[$i]['cid']; } } return false; } /** * @param string $header * @param string $value * * @return Email */ public function setHeader($header, $value) { $this->headers[$header] = str_replace(["\n", "\r"], '', $value); return $this; } /** * @param array|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; } /** * @param string $str * * @return Email */ public function setAltMessage($str) { $this->altMessage = (string) $str; return $this; } /** * @param string $type * * @return Email */ public function setMailType($type = 'text') { $this->mailType = ($type === 'html') ? 'html' : 'text'; return $this; } /** * @param bool $wordWrap * * @return Email */ public function setWordWrap($wordWrap = true) { $this->wordWrap = (bool) $wordWrap; return $this; } /** * @param string $protocol * * @return Email */ public function setProtocol($protocol = 'mail') { $this->protocol = in_array($protocol, $this->protocols, true) ? strtolower($protocol) : 'mail'; return $this; } /** * @param int $n * * @return Email */ public function setPriority($n = 3) { $this->priority = preg_match('/^[1-5]$/', (string) $n) ? (int) $n : 3; return $this; } /** * @param string $newline * * @return Email */ public function setNewline($newline = "\n") { $this->newline = in_array($newline, ["\n", "\r\n", "\r"], true) ? $newline : "\n"; return $this; } /** * @param string $CRLF * * @return Email */ public function setCRLF($CRLF = "\n") { $this->CRLF = ($CRLF !== "\n" && $CRLF !== "\r\n" && $CRLF !== "\r") ? "\n" : $CRLF; return $this; } /** * @return string */ protected function getMessageID() { $from = str_replace(['>', '<'], '', $this->headers['Return-Path']); return '<' . uniqid('', true) . strstr($from, '@') . '>'; } /** * @return string */ protected function getProtocol() { $this->protocol = strtolower($this->protocol); if (! in_array($this->protocol, $this->protocols, true)) { $this->protocol = 'mail'; } return $this->protocol; } /** * @return string */ protected function getEncoding() { if (! in_array($this->encoding, $this->bitDepths, true)) { $this->encoding = '8bit'; } foreach ($this->baseCharsets as $charset) { if (strpos($this->charset, $charset) === 0) { $this->encoding = '7bit'; break; } } return $this->encoding; } /** * @return string */ protected function getContentType() { if ($this->mailType === 'html') { return empty($this->attachments) ? 'html' : 'html-attach'; } if ($this->mailType === 'text' && ! empty($this->attachments)) { return 'plain-attach'; } 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); } /** * @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.'; } /** * @param array|string $email * * @return bool */ 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; } /** * @param string $email * * @return bool */ 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); } /** * @param array|string $email * * @return array|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('#