summaryrefslogtreecommitdiffstats
path: root/system/libraries/Email.php
diff options
context:
space:
mode:
Diffstat (limited to 'system/libraries/Email.php')
-rw-r--r--system/libraries/Email.php556
1 files changed, 350 insertions, 206 deletions
diff --git a/system/libraries/Email.php b/system/libraries/Email.php
index 459c8f590..1483f2203 100644
--- a/system/libraries/Email.php
+++ b/system/libraries/Email.php
@@ -6,7 +6,7 @@
*
* This content is released under the MIT License (MIT)
*
- * Copyright (c) 2014 - 2015, British Columbia Institute of Technology
+ * Copyright (c) 2014 - 2017, British Columbia Institute of Technology
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -28,10 +28,10 @@
*
* @package CodeIgniter
* @author EllisLab Dev Team
- * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
- * @copyright Copyright (c) 2014 - 2015, British Columbia Institute of Technology (http://bcit.ca/)
+ * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
+ * @copyright Copyright (c) 2014 - 2017, British Columbia Institute of Technology (http://bcit.ca/)
* @license http://opensource.org/licenses/MIT MIT License
- * @link http://codeigniter.com
+ * @link https://codeigniter.com
* @since Version 1.0.0
* @filesource
*/
@@ -46,7 +46,7 @@ defined('BASEPATH') OR exit('No direct script access allowed');
* @subpackage Libraries
* @category Libraries
* @author EllisLab Dev Team
- * @link http://codeigniter.com/user_guide/libraries/email.html
+ * @link https://codeigniter.com/user_guide/libraries/email.html
*/
class CI_Email {
@@ -150,13 +150,6 @@ class CI_Email {
public $charset = 'utf-8';
/**
- * Multipart message
- *
- * @var string 'mixed' (in the body) or 'related' (separate)
- */
- public $multipart = 'mixed'; // "mixed" (in the body) or "related" (separate)
-
- /**
* Alternative message (for HTML messages only)
*
* @var string
@@ -168,7 +161,7 @@ class CI_Email {
*
* @var bool
*/
- public $validate = FALSE;
+ public $validate = TRUE;
/**
* X-Priority header value.
@@ -233,13 +226,6 @@ class CI_Email {
// --------------------------------------------------------------------
/**
- * Whether PHP is running in safe mode. Initialized by the class constructor.
- *
- * @var bool
- */
- protected $_safe_mode = FALSE;
-
- /**
* Subject header
*
* @var string
@@ -261,20 +247,6 @@ class CI_Email {
protected $_finalbody = '';
/**
- * multipart/alternative boundary
- *
- * @var string
- */
- protected $_alt_boundary = '';
-
- /**
- * Attachment boundary
- *
- * @var string
- */
- protected $_atc_boundary = '';
-
- /**
* Final headers to send
*
* @var string
@@ -395,6 +367,13 @@ class CI_Email {
5 => '5 (Lowest)'
);
+ /**
+ * mbstring.func_override flag
+ *
+ * @var bool
+ */
+ protected static $func_override;
+
// --------------------------------------------------------------------
/**
@@ -408,18 +387,9 @@ class CI_Email {
public function __construct(array $config = array())
{
$this->charset = config_item('charset');
+ $this->initialize($config);
- if (count($config) > 0)
- {
- $this->initialize($config);
- }
- else
- {
- $this->_smtp_auth = ! ($this->smtp_user === '' && $this->smtp_pass === '');
- }
-
- $this->_safe_mode = ( ! is_php('5.4') && ini_get('safe_mode'));
- $this->charset = strtoupper($this->charset);
+ isset(self::$func_override) OR self::$func_override = (extension_loaded('mbstring') && ini_get('mbstring.func_override'));
log_message('info', 'Email Class Initialized');
}
@@ -427,28 +397,15 @@ class CI_Email {
// --------------------------------------------------------------------
/**
- * Destructor - Releases Resources
- *
- * @return void
- */
- public function __destruct()
- {
- if (is_resource($this->_smtp_connect))
- {
- $this->_send_command('quit');
- }
- }
-
- // --------------------------------------------------------------------
-
- /**
* Initialize preferences
*
- * @param array
+ * @param array $config
* @return CI_Email
*/
- public function initialize($config = array())
+ public function initialize(array $config = array())
{
+ $this->clear();
+
foreach ($config as $key => $val)
{
if (isset($this->$key))
@@ -465,9 +422,9 @@ class CI_Email {
}
}
}
- $this->clear();
- $this->_smtp_auth = ! ($this->smtp_user === '' && $this->smtp_pass === '');
+ $this->charset = strtoupper($this->charset);
+ $this->_smtp_auth = isset($this->smtp_user[0], $this->smtp_pass[0]);
return $this;
}
@@ -493,7 +450,6 @@ class CI_Email {
$this->_headers = array();
$this->_debug_msg = array();
- $this->set_header('User-Agent', $this->useragent);
$this->set_header('Date', $this->_set_date());
if ($clear_attachments !== FALSE)
@@ -574,14 +530,18 @@ class CI_Email {
$this->validate_email($this->_str_to_array($replyto));
}
- if ($name === '')
- {
- $name = $replyto;
- }
-
- if (strpos($name, '"') !== 0)
+ if ($name !== '')
{
- $name = '"'.$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->_prep_q_encoding($name);
+ }
}
$this->set_header('Reply-To', $name.' <'.$replyto.'>');
@@ -707,18 +667,6 @@ class CI_Email {
public function message($body)
{
$this->_body = rtrim(str_replace("\r", '', $body));
-
- /* strip slashes only if magic quotes is ON
- if we do it with magic quotes OFF, it strips real, user-inputted chars.
-
- NOTE: In PHP 5.4 get_magic_quotes_gpc() will always return 0 and
- it will probably not exist in future versions at all.
- */
- if ( ! is_php('5.4') && get_magic_quotes_gpc())
- {
- $this->_body = stripslashes($this->_body);
- }
-
return $this;
}
@@ -762,7 +710,8 @@ class CI_Email {
'name' => array($file, $newname),
'disposition' => empty($disposition) ? 'attachment' : $disposition, // Can also be 'inline' Not sure if it matters
'type' => $mime,
- 'content' => chunk_split(base64_encode($file_content))
+ 'content' => chunk_split(base64_encode($file_content)),
+ 'multipart' => 'mixed'
);
return $this;
@@ -780,15 +729,11 @@ class CI_Email {
*/
public function attachment_cid($filename)
{
- if ($this->multipart !== 'related')
- {
- $this->multipart = 'related'; // Thunderbird need this for inline images
- }
-
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]).'@');
return $this->_attachments[$i]['cid'];
}
@@ -933,19 +878,6 @@ class CI_Email {
// --------------------------------------------------------------------
/**
- * Set Message Boundary
- *
- * @return void
- */
- protected function _set_boundaries()
- {
- $this->_alt_boundary = 'B_ALT_'.uniqid(''); // multipart/alternative
- $this->_atc_boundary = 'B_ATC_'.uniqid(''); // attachment boundary
- }
-
- // --------------------------------------------------------------------
-
- /**
* Get the Message ID
*
* @return string
@@ -1012,9 +944,9 @@ class CI_Email {
{
if ($this->mailtype === 'html')
{
- return (count($this->_attachments) === 0) ? 'html' : 'html-attach';
+ return empty($this->_attachments) ? 'html' : 'html-attach';
}
- elseif ($this->mailtype === 'text' && count($this->_attachments) > 0)
+ elseif ($this->mailtype === 'text' && ! empty($this->_attachments))
{
return 'plain-attach';
}
@@ -1093,7 +1025,7 @@ class CI_Email {
{
if (function_exists('idn_to_ascii') && $atpos = strpos($email, '@'))
{
- $email = substr($email, 0, ++$atpos).idn_to_ascii(substr($email, $atpos));
+ $email = self::substr($email, 0, ++$atpos).idn_to_ascii(self::substr($email, $atpos));
}
return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
@@ -1210,7 +1142,7 @@ class CI_Email {
{
// Is the line within the allowed character count?
// If so we'll join it to the output and continue
- if (mb_strlen($line) <= $charlim)
+ if (self::strlen($line) <= $charlim)
{
$output .= $line.$this->newline;
continue;
@@ -1226,10 +1158,10 @@ class CI_Email {
}
// Trim the word down
- $temp .= mb_substr($line, 0, $charlim - 1);
- $line = mb_substr($line, $charlim - 1);
+ $temp .= self::substr($line, 0, $charlim - 1);
+ $line = self::substr($line, $charlim - 1);
}
- while (mb_strlen($line) > $charlim);
+ while (self::strlen($line) > $charlim);
// If $temp contains data it means we had to split up an over-length
// word into smaller chunks so we'll add it back to our current line
@@ -1258,10 +1190,11 @@ class CI_Email {
/**
* Build final headers
*
- * @return string
+ * @return void
*/
protected function _build_headers()
{
+ $this->set_header('User-Agent', $this->useragent);
$this->set_header('X-Sender', $this->clean_email($this->_headers['From']));
$this->set_header('X-Mailer', $this->useragent);
$this->set_header('X-Priority', $this->_priorities[$this->priority]);
@@ -1320,7 +1253,6 @@ class CI_Email {
$this->_body = $this->word_wrap($this->_body);
}
- $this->_set_boundaries();
$this->_write_headers();
$hdr = ($this->_get_protocol() === 'mail') ? $this->newline : '';
@@ -1328,7 +1260,7 @@ class CI_Email {
switch ($this->_get_content_type())
{
- case 'plain' :
+ case 'plain':
$hdr .= 'Content-Type: text/plain; charset='.$this->charset.$this->newline
.'Content-Transfer-Encoding: '.$this->_get_encoding();
@@ -1345,7 +1277,7 @@ class CI_Email {
return;
- case 'html' :
+ case 'html':
if ($this->send_multipart === FALSE)
{
@@ -1354,14 +1286,16 @@ class CI_Email {
}
else
{
- $hdr .= 'Content-Type: multipart/alternative; boundary="'.$this->_alt_boundary.'"';
+ $boundary = uniqid('B_ALT_');
+ $hdr .= 'Content-Type: multipart/alternative; boundary="'.$boundary.'"';
$body .= $this->_get_mime_message().$this->newline.$this->newline
- .'--'.$this->_alt_boundary.$this->newline
+ .'--'.$boundary.$this->newline
.'Content-Type: text/plain; charset='.$this->charset.$this->newline
.'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline.$this->newline
- .$this->_get_alt_message().$this->newline.$this->newline.'--'.$this->_alt_boundary.$this->newline
+ .$this->_get_alt_message().$this->newline.$this->newline
+ .'--'.$boundary.$this->newline
.'Content-Type: text/html; charset='.$this->charset.$this->newline
.'Content-Transfer-Encoding: quoted-printable'.$this->newline.$this->newline;
@@ -1380,14 +1314,15 @@ class CI_Email {
if ($this->send_multipart !== FALSE)
{
- $this->_finalbody .= '--'.$this->_alt_boundary.'--';
+ $this->_finalbody .= '--'.$boundary.'--';
}
return;
- case 'plain-attach' :
+ case 'plain-attach':
- $hdr .= 'Content-Type: multipart/'.$this->multipart.'; boundary="'.$this->_atc_boundary.'"';
+ $boundary = uniqid('B_ATC_');
+ $hdr .= 'Content-Type: multipart/mixed; boundary="'.$boundary.'"';
if ($this->_get_protocol() === 'mail')
{
@@ -1396,59 +1331,83 @@ class CI_Email {
$body .= $this->_get_mime_message().$this->newline
.$this->newline
- .'--'.$this->_atc_boundary.$this->newline
+ .'--'.$boundary.$this->newline
.'Content-Type: text/plain; charset='.$this->charset.$this->newline
.'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline
.$this->newline
.$this->_body.$this->newline.$this->newline;
- break;
- case 'html-attach' :
+ $this->_append_attachments($body, $boundary);
+
+ break;
+ case 'html-attach':
- $hdr .= 'Content-Type: multipart/'.$this->multipart.'; boundary="'.$this->_atc_boundary.'"';
+ $alt_boundary = uniqid('B_ALT_');
+ $last_boundary = NULL;
+
+ if ($this->_attachments_have_multipart('mixed'))
+ {
+ $atc_boundary = uniqid('B_ATC_');
+ $hdr .= 'Content-Type: multipart/mixed; boundary="'.$atc_boundary.'"';
+ $last_boundary = $atc_boundary;
+ }
+
+ if ($this->_attachments_have_multipart('related'))
+ {
+ $rel_boundary = uniqid('B_REL_');
+ $rel_boundary_header = 'Content-Type: multipart/related; boundary="'.$rel_boundary.'"';
+
+ if (isset($last_boundary))
+ {
+ $body .= '--'.$last_boundary.$this->newline.$rel_boundary_header;
+ }
+ else
+ {
+ $hdr .= $rel_boundary_header;
+ }
+
+ $last_boundary = $rel_boundary;
+ }
if ($this->_get_protocol() === 'mail')
{
$this->_header_str .= $hdr;
}
+ self::strlen($body) && $body .= $this->newline.$this->newline;
$body .= $this->_get_mime_message().$this->newline.$this->newline
- .'--'.$this->_atc_boundary.$this->newline
+ .'--'.$last_boundary.$this->newline
- .'Content-Type: multipart/alternative; boundary="'.$this->_alt_boundary.'"'.$this->newline.$this->newline
- .'--'.$this->_alt_boundary.$this->newline
+ .'Content-Type: multipart/alternative; boundary="'.$alt_boundary.'"'.$this->newline.$this->newline
+ .'--'.$alt_boundary.$this->newline
.'Content-Type: text/plain; charset='.$this->charset.$this->newline
.'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline.$this->newline
- .$this->_get_alt_message().$this->newline.$this->newline.'--'.$this->_alt_boundary.$this->newline
+ .$this->_get_alt_message().$this->newline.$this->newline
+ .'--'.$alt_boundary.$this->newline
.'Content-Type: text/html; charset='.$this->charset.$this->newline
.'Content-Transfer-Encoding: quoted-printable'.$this->newline.$this->newline
.$this->_prep_quoted_printable($this->_body).$this->newline.$this->newline
- .'--'.$this->_alt_boundary.'--'.$this->newline.$this->newline;
-
- break;
- }
+ .'--'.$alt_boundary.'--'.$this->newline.$this->newline;
- $attachment = array();
- for ($i = 0, $c = count($this->_attachments), $z = 0; $i < $c; $i++)
- {
- $filename = $this->_attachments[$i]['name'][0];
- $basename = ($this->_attachments[$i]['name'][1] === NULL)
- ? basename($filename) : $this->_attachments[$i]['name'][1];
+ if ( ! empty($rel_boundary))
+ {
+ $body .= $this->newline.$this->newline;
+ $this->_append_attachments($body, $rel_boundary, 'related');
+ }
- $attachment[$z++] = '--'.$this->_atc_boundary.$this->newline
- .'Content-type: '.$this->_attachments[$i]['type'].'; '
- .'name="'.$basename.'"'.$this->newline
- .'Content-Disposition: '.$this->_attachments[$i]['disposition'].';'.$this->newline
- .'Content-Transfer-Encoding: base64'.$this->newline
- .(empty($this->_attachments[$i]['cid']) ? '' : 'Content-ID: <'.$this->_attachments[$i]['cid'].'>'.$this->newline);
+ // multipart/mixed attachments
+ if ( ! empty($atc_boundary))
+ {
+ $body .= $this->newline.$this->newline;
+ $this->_append_attachments($body, $atc_boundary, 'mixed');
+ }
- $attachment[$z++] = $this->_attachments[$i]['content'];
+ break;
}
- $body .= implode($this->newline, $attachment).$this->newline.'--'.$this->_atc_boundary.'--';
$this->_finalbody = ($this->_get_protocol() === 'mail')
? $body
: $hdr.$this->newline.$this->newline.$body;
@@ -1458,6 +1417,58 @@ class CI_Email {
// --------------------------------------------------------------------
+ protected function _attachments_have_multipart($type)
+ {
+ foreach ($this->_attachments as &$attachment)
+ {
+ if ($attachment['multipart'] === $type)
+ {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Prepares attachment string
+ *
+ * @param string $body Message body to append to
+ * @param string $boundary Multipart boundary
+ * @param string $multipart When provided, only attachments of this type will be processed
+ * @return string
+ */
+ protected function _append_attachments(&$body, $boundary, $multipart = null)
+ {
+ for ($i = 0, $c = count($this->_attachments); $i < $c; $i++)
+ {
+ if (isset($multipart) && $this->_attachments[$i]['multipart'] !== $multipart)
+ {
+ continue;
+ }
+
+ $name = isset($this->_attachments[$i]['name'][1])
+ ? $this->_attachments[$i]['name'][1]
+ : basename($this->_attachments[$i]['name'][0]);
+
+ $body .= '--'.$boundary.$this->newline
+ .'Content-Type: '.$this->_attachments[$i]['type'].'; name="'.$name.'"'.$this->newline
+ .'Content-Disposition: '.$this->_attachments[$i]['disposition'].';'.$this->newline
+ .'Content-Transfer-Encoding: base64'.$this->newline
+ .(empty($this->_attachments[$i]['cid']) ? '' : 'Content-ID: <'.$this->_attachments[$i]['cid'].'>'.$this->newline)
+ .$this->newline
+ .$this->_attachments[$i]['content'].$this->newline;
+ }
+
+ // $name won't be set if no attachments were appended,
+ // and therefore a boundary wouldn't be necessary
+ empty($name) OR $body .= '--'.$boundary.'--';
+ }
+
+ // --------------------------------------------------------------------
+
/**
* Prep Quoted Printable
*
@@ -1469,6 +1480,20 @@ class CI_Email {
*/
protected function _prep_quoted_printable($str)
{
+ // ASCII code numbers for "safe" characters that can always be
+ // used literally, without encoding, as described in RFC 2049.
+ // http://www.ietf.org/rfc/rfc2049.txt
+ static $ascii_safe_chars = array(
+ // ' ( ) + , - . / : = ?
+ 39, 40, 41, 43, 44, 45, 46, 47, 58, 61, 63,
+ // numbers
+ 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
+ // upper-case letters
+ 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
+ // lower-case letters
+ 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122
+ );
+
// We are intentionally wrapping so mail servers will encode characters
// properly and MUAs will behave, so {unwrap} must go!
$str = str_replace(array('{unwrap}', '{/unwrap}'), '', $str);
@@ -1479,14 +1504,7 @@ class CI_Email {
// which only works with "\n".
if ($this->crlf === "\r\n")
{
- if (is_php('5.3'))
- {
- return quoted_printable_encode($str);
- }
- elseif (function_exists('imap_8bit'))
- {
- return imap_8bit($str);
- }
+ return quoted_printable_encode($str);
}
// Reduce multiple spaces & remove nulls
@@ -1503,7 +1521,7 @@ class CI_Email {
foreach (explode("\n", $str) as $line)
{
- $length = strlen($line);
+ $length = self::strlen($line);
$temp = '';
// Loop through each character in the line to add soft-wrap
@@ -1516,18 +1534,29 @@ class CI_Email {
$ascii = ord($char);
// Convert spaces and tabs but only if it's the end of the line
- if ($i === ($length - 1) && ($ascii === 32 OR $ascii === 9))
+ if ($ascii === 32 OR $ascii === 9)
{
- $char = $escape.sprintf('%02s', dechex($ascii));
+ if ($i === ($length - 1))
+ {
+ $char = $escape.sprintf('%02s', dechex($ascii));
+ }
}
- elseif ($ascii === 61) // encode = signs
+ // DO NOT move this below the $ascii_safe_chars line!
+ //
+ // = (equals) signs are allowed by RFC2049, but must be encoded
+ // as they are the encoding delimiter!
+ elseif ($ascii === 61)
{
$char = $escape.strtoupper(sprintf('%02s', dechex($ascii))); // =3D
}
+ elseif ( ! in_array($ascii, $ascii_safe_chars, TRUE))
+ {
+ $char = $escape.strtoupper(sprintf('%02s', dechex($ascii)));
+ }
// If we're at the character limit, add the line to the output,
// reset our temp variable, and keep on chuggin'
- if ((strlen($temp) + strlen($char)) >= 76)
+ if ((self::strlen($temp) + self::strlen($char)) >= 76)
{
$output .= $temp.$escape.$this->crlf;
$temp = '';
@@ -1542,7 +1571,7 @@ class CI_Email {
}
// get rid of extra CRLF tacked onto the end
- return substr($output, 0, strlen($this->crlf) * -1);
+ return self::substr($output, 0, self::strlen($this->crlf) * -1);
}
// --------------------------------------------------------------------
@@ -1563,11 +1592,10 @@ class CI_Email {
if ($this->charset === 'UTF-8')
{
- if (MB_ENABLED === TRUE)
- {
- return mb_encode_mimeheader($str, $this->charset, 'Q', $this->crlf);
- }
- elseif (ICONV_ENABLED === TRUE)
+ // Note: We used to have mb_encode_mimeheader() as the first choice
+ // here, but it turned out to be buggy and unreliable. DO NOT
+ // re-add it! -- Narf
+ if (ICONV_ENABLED === TRUE)
{
$output = @iconv_mime_encode('', $str,
array(
@@ -1585,18 +1613,22 @@ class CI_Email {
// iconv_mime_encode() will always put a header field name.
// We've passed it an empty one, but it still prepends our
// encoded string with ': ', so we need to strip it.
- return substr($output, 2);
+ return self::substr($output, 2);
}
$chars = iconv_strlen($str, 'UTF-8');
}
+ elseif (MB_ENABLED === TRUE)
+ {
+ $chars = mb_strlen($str, 'UTF-8');
+ }
}
// We might already have this set for UTF-8
- isset($chars) OR $chars = strlen($str);
+ isset($chars) OR $chars = self::strlen($str);
$output = '=?'.$this->charset.'?Q?';
- for ($i = 0, $length = strlen($output); $i < $chars; $i++)
+ for ($i = 0, $length = self::strlen($output); $i < $chars; $i++)
{
$chr = ($this->charset === 'UTF-8' && ICONV_ENABLED === TRUE)
? '='.implode('=', str_split(strtoupper(bin2hex(iconv_substr($str, $i, 1, $this->charset))), 2))
@@ -1604,11 +1636,11 @@ class CI_Email {
// RFC 2045 sets a limit of 76 characters per line.
// We'll append ?= to the end of each line though.
- if ($length + ($l = strlen($chr)) > 74)
+ if ($length + ($l = self::strlen($chr)) > 74)
{
$output .= '?='.$this->crlf // EOL
.' =?'.$this->charset.'?Q?'.$chr; // New line
- $length = 6 + strlen($this->charset) + $l; // Reset the length for the new line
+ $length = 6 + self::strlen($this->charset) + $l; // Reset the length for the new line
}
else
{
@@ -1701,14 +1733,14 @@ class CI_Email {
if ($i === $float)
{
- $chunk[] = substr($set, 1);
+ $chunk[] = self::substr($set, 1);
$float += $this->bcc_batch_size;
$set = '';
}
if ($i === $c-1)
{
- $chunk[] = substr($set, 1);
+ $chunk[] = self::substr($set, 1);
}
}
@@ -1791,6 +1823,33 @@ class CI_Email {
// --------------------------------------------------------------------
/**
+ * Validate email for shell
+ *
+ * Applies stricter, shell-safe validation to email addresses.
+ * Introduced to prevent RCE via sendmail's -f option.
+ *
+ * @see https://github.com/bcit-ci/CodeIgniter/issues/4963
+ * @see https://gist.github.com/Zenexer/40d02da5e07f151adeaeeaa11af9ab36
+ * @license https://creativecommons.org/publicdomain/zero/1.0/ CC0 1.0, Public Domain
+ *
+ * Credits for the base concept go to Paul Buonopane <paul@namepros.com>
+ *
+ * @param string $email
+ * @return bool
+ */
+ protected function _validate_email_for_shell(&$email)
+ {
+ if (function_exists('idn_to_ascii') && $atpos = strpos($email, '@'))
+ {
+ $email = self::substr($email, 0, ++$atpos).idn_to_ascii(self::substr($email, $atpos));
+ }
+
+ return (filter_var($email, FILTER_VALIDATE_EMAIL) === $email && preg_match('#\A[a-z0-9._+-]+@[a-z0-9.-]{1,253}\z#i', $email));
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
* Send using mail()
*
* @return bool
@@ -1802,16 +1861,18 @@ class CI_Email {
$this->_recipients = implode(', ', $this->_recipients);
}
- if ($this->_safe_mode === TRUE)
+ // _validate_email_for_shell() below accepts by reference,
+ // so this needs to be assigned to a variable
+ $from = $this->clean_email($this->_headers['Return-Path']);
+
+ if ( ! $this->_validate_email_for_shell($from))
{
return mail($this->_recipients, $this->_subject, $this->_finalbody, $this->_header_str);
}
- else
- {
- // most documentation of sendmail using the "-f" flag lacks a space after it, however
- // we've encountered servers that seem to require it to be in place.
- return mail($this->_recipients, $this->_subject, $this->_finalbody, $this->_header_str, '-f '.$this->clean_email($this->_headers['Return-Path']));
- }
+
+ // most documentation of sendmail using the "-f" flag lacks a space after it, however
+ // we've encountered servers that seem to require it to be in place.
+ return mail($this->_recipients, $this->_subject, $this->_finalbody, $this->_header_str, '-f '.$from);
}
// --------------------------------------------------------------------
@@ -1823,14 +1884,22 @@ class CI_Email {
*/
protected function _send_with_sendmail()
{
+ // _validate_email_for_shell() below accepts by reference,
+ // so this needs to be assigned to a variable
+ $from = $this->clean_email($this->_headers['From']);
+ if ($this->_validate_email_for_shell($from))
+ {
+ $from = '-f '.$from;
+ }
+ else
+ {
+ $from = '';
+ }
+
// is popen() enabled?
- if ( ! function_usable('popen')
- OR FALSE === ($fp = @popen(
- $this->mailpath.' -oi -f '.$this->clean_email($this->_headers['From'])
- .' -t -r '.$this->clean_email($this->_headers['Return-Path'])
- , 'w'))
- ) // server probably has popen disabled, so nothing we can do to get a verbose error.
+ if ( ! function_usable('popen') OR FALSE === ($fp = @popen($this->mailpath.' -oi '.$from.' -t', 'w')))
{
+ // server probably has popen disabled, so nothing we can do to get a verbose error.
return FALSE;
}
@@ -1869,20 +1938,29 @@ class CI_Email {
return FALSE;
}
- $this->_send_command('from', $this->clean_email($this->_headers['From']));
+ if ( ! $this->_send_command('from', $this->clean_email($this->_headers['From'])))
+ {
+ $this->_smtp_end();
+ return FALSE;
+ }
foreach ($this->_recipients as $val)
{
- $this->_send_command('to', $val);
+ if ( ! $this->_send_command('to', $val))
+ {
+ $this->_smtp_end();
+ return FALSE;
+ }
}
if (count($this->_cc_array) > 0)
{
foreach ($this->_cc_array as $val)
{
- if ($val !== '')
+ if ($val !== '' && ! $this->_send_command('to', $val))
{
- $this->_send_command('to', $val);
+ $this->_smtp_end();
+ return FALSE;
}
}
}
@@ -1891,14 +1969,19 @@ class CI_Email {
{
foreach ($this->_bcc_array as $val)
{
- if ($val !== '')
+ if ($val !== '' && ! $this->_send_command('to', $val))
{
- $this->_send_command('to', $val);
+ $this->_smtp_end();
+ return FALSE;
}
}
}
- $this->_send_command('data');
+ if ( ! $this->_send_command('data'))
+ {
+ $this->_smtp_end();
+ return FALSE;
+ }
// perform dot transformation on any lines that begin with a dot
$this->_send_data($this->_header_str.preg_replace('/^\./m', '..$1', $this->_finalbody));
@@ -1906,30 +1989,38 @@ class CI_Email {
$this->_send_data('.');
$reply = $this->_get_smtp_data();
-
$this->_set_error_message($reply);
+ $this->_smtp_end();
+
if (strpos($reply, '250') !== 0)
{
$this->_set_error_message('lang:email_smtp_error', $reply);
return FALSE;
}
- if ($this->smtp_keepalive)
- {
- $this->_send_command('reset');
- }
- else
- {
- $this->_send_command('quit');
- }
-
return TRUE;
}
// --------------------------------------------------------------------
/**
+ * SMTP End
+ *
+ * Shortcut to send RSET or QUIT depending on keep-alive
+ *
+ * @return void
+ */
+ protected function _smtp_end()
+ {
+ ($this->smtp_keepalive)
+ ? $this->_send_command('reset')
+ : $this->_send_command('quit');
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
* SMTP Connect
*
* @return string
@@ -1982,7 +2073,7 @@ class CI_Email {
*
* @param string
* @param string
- * @return string
+ * @return bool
*/
protected function _send_command($cmd, $data = '')
{
@@ -2045,7 +2136,7 @@ class CI_Email {
$this->_debug_msg[] = '<pre>'.$cmd.': '.$reply.'</pre>';
- if ((int) substr($reply, 0, 3) !== $resp)
+ if ((int) self::substr($reply, 0, 3) !== $resp)
{
$this->_set_error_message('lang:email_smtp_error', $reply);
return FALSE;
@@ -2113,6 +2204,11 @@ class CI_Email {
return FALSE;
}
+ if ($this->smtp_keepalive)
+ {
+ $this->_smtp_auth = FALSE;
+ }
+
return TRUE;
}
@@ -2127,9 +2223,9 @@ class CI_Email {
protected function _send_data($data)
{
$data .= $this->newline;
- for ($written = $timestamp = 0, $length = strlen($data); $written < $length; $written += $result)
+ for ($written = $timestamp = 0, $length = self::strlen($data); $written < $length; $written += $result)
{
- if (($result = fwrite($this->_smtp_connect, substr($data, $written))) === FALSE)
+ if (($result = fwrite($this->_smtp_connect, self::substr($data, $written))) === FALSE)
{
break;
}
@@ -2302,4 +2398,52 @@ class CI_Email {
return 'application/x-unknown-content-type';
}
+ // --------------------------------------------------------------------
+
+ /**
+ * Destructor
+ *
+ * @return void
+ */
+ public function __destruct()
+ {
+ is_resource($this->_smtp_connect) && $this->_send_command('quit');
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Byte-safe strlen()
+ *
+ * @param string $str
+ * @return int
+ */
+ protected static function strlen($str)
+ {
+ return (self::$func_override)
+ ? mb_strlen($str, '8bit')
+ : strlen($str);
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Byte-safe substr()
+ *
+ * @param string $str
+ * @param int $start
+ * @param int $length
+ * @return string
+ */
+ protected static function substr($str, $start, $length = NULL)
+ {
+ if (self::$func_override)
+ {
+ return mb_substr($str, $start, $length, '8bit');
+ }
+
+ return isset($length)
+ ? substr($str, $start, $length)
+ : substr($str, $start);
+ }
}