Changeset 774 for trunk


Ignore:
Timestamp:
Jul 24, 2022 4:01:37 PM (20 months ago)
Author:
anonymous
Message:

Add Email::setRawBody() for messages manually composed and encoded outside of this Email class, and need to bypass replacements, detection of mail header injection attacks, and the encoding provided by mb_send_mail().

Location:
trunk/lib
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/lib/Email.inc.php

    r770 r774  
    6161        'from' => null,
    6262        'subject' => null,
    63         'headers' => null,
     63        'headers' => [],
    6464        'envelope_sender_address' => null, // AKA the bounce-to address. Will default to 'from' if left null.
    6565        'regex' => null,
     
    8686    // String that contains the email body after replacements.
    8787    protected $_template_replaced;
     88
     89    // String that contains the final email body.
     90    // Use only when email is manually composed and encoded outside of this Email class, and need to bypass replacements, detection of mail header injection attacks, and the encoding provided by mb_send_mail().
     91    protected $_raw_body;
    8892
    8993    // Email debug modes.
     
    175179    }
    176180
     181    /*
     182    * Append headers to $this->_params['headers']
     183    *
     184    * @access   public
     185    * @param    array   $headers    Array of key=>val pairs that will be appended.
     186    * @return   void
     187    * @author   Quinn Comendant <quinn@strangecode.com>
     188    * @since    23 Jul 2022 13:36:21
     189    */
     190    public function appendHeaders($headers)
     191    {
     192        $app =& App::getInstance();
     193        if (!isset($headers) || !is_array($headers)) {
     194            $app->logMsg(sprintf('%s requires an array of header values', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
     195        }
     196
     197        $this->_params['headers'] = array_merge($this->_params['headers'], $headers);
     198    }
     199
    177200    /**
    178201     * Loads template from file to generate email body.
     
    188211
    189212        // Load file, using include_path.
    190         if (!$this->_template = file_get_contents($template, true)) {
    191             $app->logMsg(sprintf('Email template file does not exist: %s', $template), LOG_ERR, __FILE__, __LINE__);
     213        $this->_template = file_get_contents($template, true);
     214        if (!$this->_template || '' == trim($this->_template)) {
     215            $app->logMsg(sprintf('Email template file does not exist or is empty: %s', $template), LOG_ERR, __FILE__, __LINE__);
    192216            $this->_template = null;
    193217            $this->_template_replaced = null;
     218            $this->_raw_body = null;
    194219            return false;
    195220        }
     
    203228        // This could be a new template, so reset the _template_replaced.
    204229        $this->_template_replaced = null;
     230        $this->_raw_body = null;
    205231
    206232        $this->_template_filename = $template;
     
    213239     *
    214240     * @access  public
    215      * @param   string  $template   Filename of email template.
     241     * @param   string  $string   Content of email template.
    216242     * @author  Quinn Comendant <quinn@strangecode.com>
    217243     * @since   28 Nov 2005 12:56:23
     
    222248
    223249        if ('' == trim($string)) {
    224             $app->logMsg(sprintf('Empty string provided.', null), LOG_ERR, __FILE__, __LINE__);
     250            $app->logMsg(sprintf('Empty string provided to %s.', __METHOD__), LOG_ERR, __FILE__, __LINE__);
    225251            $this->_template_replaced = null;
     252            $this->_raw_body = null;
    226253            return false;
    227254        } else {
     
    229256            // This could be a new template, so reset the _template_replaced.
    230257            $this->_template_replaced = null;
     258            $this->_raw_body = null;
    231259
    232260            $this->_template_filename = '(using Email::setString)';
     
    271299        // Search and replace all values at once.
    272300        $this->_template_replaced = str_replace($search, $replace, $this->_template);
     301    }
     302
     303    /**
     304     * Set the final email body to send. Using this disables any further post-processing, including encoding and scanning for mail header injection attacks.
     305     *
     306     * @access  public
     307     * @param   string  $body   Final email body.
     308     * @author  Quinn Comendant <quinn@strangecode.com>
     309     * @since   23 Jul 2022 12:01:52
     310     */
     311    public function setRawBody($string)
     312    {
     313        $app =& App::getInstance();
     314
     315        if ('' == trim($string)) {
     316            $app->logMsg(sprintf('Empty string provided to %s.', __METHOD__), LOG_ERR, __FILE__, __LINE__);
     317            $this->_template = null;
     318            $this->_template_replaced = null;
     319            $this->_raw_body = null;
     320            return false;
     321        } else {
     322            $this->_template = null;
     323            $this->_template_replaced = null;
     324            $this->_raw_body = $string;
     325
     326            $this->_template_filename = '(using Email::setRawBody)';
     327
     328            return true;
     329        }
    273330    }
    274331
     
    290347        $app =& App::getInstance();
    291348
     349        if (isset($this->_raw_body)) {
     350            return $this->_raw_body;
     351        }
     352
    292353        $final_body = isset($this->_template_replaced) ? $this->_template_replaced : $this->_template;
    293354        // Ensure all placeholders have been replaced. Find anything with {...} characters.
     
    332393            $app->logMsg('Cannot send email. SUBJECT not defined.', LOG_ERR, __FILE__, __LINE__);
    333394            return false;
    334         } else if (!isset($this->_template)) {
    335             $app->logMsg(sprintf('Cannot send email: "%s". Template not set.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
     395        } else if (!isset($this->_template) && !isset($this->_raw_body)) {
     396            $app->logMsg(sprintf('Cannot send email: "%s". Need a template or raw body.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
    336397            return false;
    337398        } else if (!isset($this->_params['to'])) {
     
    343404        }
    344405
    345         // Wrap email text body, using _template_replaced if replacements have been used, or just a fresh _template if not.
    346         $final_body = isset($this->_template_replaced) ? $this->_template_replaced : $this->_template;
    347         if (false !== $this->getParam('wrap')) {
    348             $final_body = wordwrap($final_body, $this->getParam('line_length'), $this->getParam('crlf'));
    349         }
    350 
    351         // Ensure all placeholders have been replaced. Find anything with {...} characters.
    352         if (preg_match('/({[^}]+})/', $final_body, $unreplaced_match)) {
    353             unset($unreplaced_match[0]);
    354             $app->logMsg(sprintf('Unreplaced variable(s) "%s" in template "%s"', getDump($unreplaced_match), $this->_template_filename), LOG_ERR, __FILE__, __LINE__);
    355             return false;
    356         }
    357 
    358         // Final "to" header can have multiple addresses if in an array.
     406        // Final “to” header can have multiple addresses if in an array.
    359407        $final_to = is_array($this->_params['to']) ? join(', ', $this->_params['to']) : $this->_params['to'];
    360 
    361         // From headers are custom headers.
    362         $headers = array('From' => $this->_params['from']);
    363 
    364         // Additional headers.
    365         if (isset($this->_params['headers']) && is_array($this->_params['headers'])) {
    366             $headers = array_merge($this->_params['headers'], $headers);
    367         }
     408        if (!preg_match('/(?:>$|>,)/', $final_to)) {
     409            $app->logMsg(sprintf('Email addresses should be enclosed in <angle brackets>: %s', $final_to), LOG_NOTICE, __FILE__, __LINE__);
     410        }
     411
     412        // “From” headers are custom headers.
     413        $this->appendHeaders(['From' => $this->_params['from']]);
    368414
    369415        // Process headers.
    370416        $final_headers_arr = array();
    371417        $final_headers = '';
    372         foreach ($headers as $key => $val) {
     418        foreach ($this->_params['headers'] as $key => $val) {
    373419            // Validate key and values.
    374420            if (!isset($val) || !strlen($val)) {
     
    376422                continue;
    377423            }
    378             if (!strlen($key) || preg_match("/[\n\r]/", $key . $val) || preg_match('/[^\w-]/', $key)) {
     424            // Ensure headers meet RFC requirements.
     425            // https://datatracker.ietf.org/doc/html/rfc5322#section-2.1.1
     426            // https://datatracker.ietf.org/doc/html/rfc5322#section-2.2
     427            if (!strlen($key) || strlen($key . $val) > 998 || preg_match('/[^\x21-\x39\x3B-\x7E]/', $key)) {
    379428                $app->logMsg(sprintf('Broken email header provided: %s=%s', $key, $val), LOG_WARNING, __FILE__, __LINE__);
    380429                continue;
     
    407456        }
    408457
    409         // Check for mail header injection attacks.
    410         $full_mail_content = join($this->getParam('crlf'), array($final_to, $this->_params['subject'], $final_body));
    411         if (preg_match("/(^|[\n\r])(Content-Type|MIME-Version|Content-Transfer-Encoding|Bcc|Cc)\s*:/i", $full_mail_content)) {
    412             $app->logMsg(sprintf('Mail header injection attack in content: %s', $full_mail_content), LOG_WARNING, __FILE__, __LINE__);
    413             return false;
     458        if (isset($this->_raw_body)) {
     459            $mail_function = 'mail';
     460            $final_body = $this->_raw_body;
     461        } else {
     462            $mail_function = 'mb_send_mail';
     463
     464            // Wrap email text body, using _template_replaced if replacements have been used, or just a fresh _template if not.
     465            $final_body = isset($this->_template_replaced) ? $this->_template_replaced : $this->_template;
     466            if (false !== $this->getParam('wrap')) {
     467                $final_body = wordwrap($final_body, $this->getParam('line_length'), $this->getParam('crlf'));
     468            }
     469
     470            // Ensure all placeholders have been replaced. Find anything with {...} characters.
     471            if (preg_match('/({[^}]+})/', $final_body, $unreplaced_match)) {
     472                unset($unreplaced_match[0]);
     473                $app->logMsg(sprintf('Unreplaced variable(s) "%s" in template "%s"', getDump($unreplaced_match), $this->_template_filename), LOG_ERR, __FILE__, __LINE__);
     474                return false;
     475            }
     476
     477            // Check for mail header injection attacks.
     478            $full_mail_content = join($this->getParam('crlf'), array($final_to, $this->_params['subject'], $final_body));
     479            if (preg_match("/(^|[\n\r])(Content-Type|MIME-Version|Content-Transfer-Encoding|Bcc|Cc)\s*:/i", $full_mail_content)) {
     480                $app->logMsg(sprintf('Mail header injection attack in content: %s', $full_mail_content), LOG_WARNING, __FILE__, __LINE__);
     481                return false;
     482            }
    414483        }
    415484
     
    440509        // Send email without 5th parameter if safemode is enabled.
    441510        if (ini_get('safe_mode')) {
    442             $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers);
    443         } else {
    444             $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers, $additional_parameter);
     511            $ret = $mail_function($final_to, $this->_params['subject'], $final_body, $final_headers);
     512        } else {
     513            $ret = $mail_function($final_to, $this->_params['subject'], $final_body, $final_headers, $additional_parameter);
    445514        }
    446515
  • trunk/lib/Utilities.inc.php

    r773 r774  
    287287    $replace['_apostrophes']     = '’';
    288288
    289     // double--hyphens  →  en — dashes
     289    // double--hyphens  →  en – dashes
    290290    $search['_em_dash']          = '/(?<=[\w\s"\'”’)])--(?=[\w\s“”‘"\'(?])/imsu';
    291291    $replace['_em_dash']         = ' – ';
Note: See TracChangeset for help on using the changeset viewer.