source: trunk/lib/Email.inc.php @ 558

Last change on this file since 558 was 550, checked in by anonymous, 9 years ago

Escaped quotes from email from names.
Changed logMsg string truncation method and added version to email log msg.
Better variable testing in carry queries.
Spelling errors.
Added runtime cache to Currency.
Added logging to form validation.
More robust form validation.
Added json serialization methond to Version.

File size: 17.6 KB
RevLine 
[23]1<?php
2/**
[362]3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
[396]5 * Copyright 2001-2012 Strangecode, LLC
[454]6 *
[362]7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
[454]13 *
[362]14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
[454]18 *
[362]19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
[460]23/*
24* Email.inc.php
25*
26* Easy email template usage.
27*
28* @author  Quinn Comendant <quinn@strangecode.com>
29* @version 1.0
30*
31* Example of use:
32---------------------------------------------------------------------
33// Setup email object.
[23]34$email = new Email(array(
35    'to' => array($frm['email'], 'q@lovemachine.local'),
[550]36    'from' => sprintf('"%s" <%s>', addcslashes($app->getParam('site_name'), '"'), $app->getParam('site_email')),
[24]37    'subject' => 'Your account has been activated',
[23]38));
39$email->setTemplate('email_registration_confirm.ihtml');
[41]40// $email->setString('Or you can pass your message body as a string, also with {VARIABLES}.');
[23]41$email->replace(array(
[136]42    'site_name' => $app->getParam('site_name'),
43    'site_url' => $app->getParam('site_url'),
[23]44    'username' => $frm['username'],
45    'password' => $frm['password1'],
46));
47if ($email->send()) {
[136]48    $app->raiseMsg(sprintf(_("A confirmation email has been sent to %s."), $frm['email']), MSG_SUCCESS, __FILE__, __LINE__);
[23]49} else {
[136]50    $app->logMsg(sprintf('Error sending confirmation email to address %s', $frm['email']), LOG_NOTICE, __FILE__, __LINE__);
[23]51}
[460]52---------------------------------------------------------------------
53*/
[136]54
[502]55class Email
56{
[23]57
[24]58    // Default parameters, to be overwritten by setParam() and read with getParam()
[484]59    protected $_params = array(
[23]60        'to' => null,
61        'from' => null,
62        'subject' => null,
[38]63        'headers' => null,
[490]64        'envelope_sender_address' => null, // AKA the bounce-to address. Will default to 'from' if left null.
[212]65        'regex' => null,
[454]66
[212]67        // A single carriage return (\n) should terminate lines for locally injected mail.
68        // A carriage return + line-feed (\r\n) should be used if sending mail directly with SMTP.
69        'crlf' => "\n",
[454]70
[212]71        // RFC 2822 says line length MUST be no more than 998 characters, and SHOULD be no more than 78 characters, excluding the CRLF.
72        // http://mailformat.dan.info/body/linelength.html
73        'wrap' => true,
74        'line_length' => 75,
[23]75    );
[42]76
[24]77    // String that contains the email body.
[484]78    protected $_template;
[42]79
[24]80    // String that contains the email body after replacements.
[484]81    protected $_template_replaced;
[23]82
83    /**
84     * Constructor.
85     *
86     * @access  public
87     * @param   array   $params     Array of object parameters.
88     * @author  Quinn Comendant <quinn@strangecode.com>
89     * @since   28 Nov 2005 12:59:41
90     */
[468]91    public function __construct($params=null)
[23]92    {
[334]93        // The regex used in validEmail(). Set here instead of in the default _params above so we can use the concatenation . dot.
[357]94        // This matches a (valid) email address as complex as:
[399]95        //      "Jane & Bob Smith" <bob&smith's/dep=sales!@smith-wick.ca.us > (Sales department)
[24]96        // ...and something as simple as:
97        //      x@x.com
[399]98        $this->setParam(array('regex' => '/^(?:(?:"[^"]*?"\s*|[^,@]*)(<\s*)|(?:"[^"]*?"|[^,@]*)\s+|)'   // Display name
[357]99        . '((?:[^.<>\s@",\[\]]+[^<>\s@",\[\]])*[^.<>\s@",\[\]]+)'       // Local-part
[23]100        . '@'                                                           // @
101        . '((?:(\[)|[A-Z0-9]?)'                                         // Domain, first char
102        . '(?(4)'                                                       // Domain conditional for if first domain char is [
103        . '(?:[0-9]{1,3}\.){3}[0-9]{1,3}\]'                             // TRUE, matches IP address
104        . '|'
105        . '[.-]?(?:[A-Z0-9]+[-.])*(?:[A-Z0-9]+\.)+[A-Z]{2,6}))'         // FALSE, matches domain name
106        . '(?(1)'                                                       // Comment conditional for if initial < exists
[490]107        . '(?:\s*>\s*|>\s+\([^,@]+\)\s*)'                               // TRUE, ensure ending >
[23]108        . '|'
109        . '(?:|\s*|\s+\([^,@]+\)\s*))$/i'));                            // FALSE ensure there is no ending >
110
[25]111        if (isset($params)) {
112            $this->setParam($params);
113        }
[23]114    }
115
116    /**
117     * Set (or overwrite existing) parameters by passing an array of new parameters.
118     *
119     * @access public
120     * @param  array    $params     Array of parameters (key => val pairs).
121     */
[468]122    public function setParam($params)
[23]123    {
[479]124        $app =& App::getInstance();
[454]125
[23]126        if (isset($params) && is_array($params)) {
127            // Enforce valid email addresses.
128            if (isset($params['to']) && !$this->validEmail($params['to'])) {
129                $params['to'] = null;
130            }
131            if (isset($params['from']) && !$this->validEmail($params['from'])) {
132                $params['from'] = null;
133            }
[490]134            if (isset($params['envelope_sender_address']) && !$this->validEmail($params['envelope_sender_address'])) {
135                $params['envelope_sender_address'] = null;
136            }
[23]137
138            // Merge new parameters with old overriding only those passed.
139            $this->_params = array_merge($this->_params, $params);
140        } else {
[136]141            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
[23]142        }
143    }
144
145    /**
146     * Return the value of a parameter, if it exists.
147     *
148     * @access public
149     * @param string $param        Which parameter to return.
150     * @return mixed               Configured parameter value.
151     */
[468]152    public function getParam($param)
[23]153    {
[479]154        $app =& App::getInstance();
[454]155
[478]156        if (array_key_exists($param, $this->_params)) {
[23]157            return $this->_params[$param];
158        } else {
[146]159            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
[23]160            return null;
161        }
162    }
163
164    /**
165     * Loads template from file to generate email body.
166     *
167     * @access  public
168     * @param   string  $template   Filename of email template.
169     * @author  Quinn Comendant <quinn@strangecode.com>
170     * @since   28 Nov 2005 12:56:23
171     */
[468]172    public function setTemplate($template)
[23]173    {
[479]174        $app =& App::getInstance();
[454]175
[23]176        // Load file, using include_path.
177        if (!$this->_template = file_get_contents($template, true)) {
[136]178            $app->logMsg(sprintf('Email template file does not exist: %s', $template), LOG_ERR, __FILE__, __LINE__);
[23]179            $this->_template = null;
180            $this->_template_replaced = null;
181            return false;
182        }
[24]183        // This could be a new template, so reset the _template_replaced.
[23]184        $this->_template_replaced = null;
185        return true;
186    }
187
188    /**
189     * Loads template from string to generate email body.
190     *
191     * @access  public
192     * @param   string  $template   Filename of email template.
193     * @author  Quinn Comendant <quinn@strangecode.com>
194     * @since   28 Nov 2005 12:56:23
195     */
[468]196    public function setString($string)
[23]197    {
[479]198        $app =& App::getInstance();
[454]199
[41]200        if ('' == trim($string)) {
[136]201            $app->logMsg(sprintf('Empty string provided.', null), LOG_ERR, __FILE__, __LINE__);
[23]202            $this->_template_replaced = null;
203            return false;
204        } else {
[41]205            $this->_template = $string;
[24]206            // This could be a new template, so reset the _template_replaced.
[23]207            $this->_template_replaced = null;
208            return true;
209        }
210    }
[42]211
[23]212    /**
213     * Replace variables in template with argument data.
214     *
215     * @access  public
216     * @param   array   $replacements   Array keys are the values to search for, array vales are the replacement values.
217     * @author  Quinn Comendant <quinn@strangecode.com>
218     * @since   28 Nov 2005 13:08:51
219     */
[468]220    public function replace($replacements)
[23]221    {
[479]222        $app =& App::getInstance();
[454]223
[24]224        // Ensure template exists.
[23]225        if (!isset($this->_template)) {
[136]226            $app->logMsg(sprintf('Cannot replace variables, no template defined.', null), LOG_ERR, __FILE__, __LINE__);
[42]227            return false;
[23]228        }
[42]229
[24]230        // Ensure replacements argument is an array.
[23]231        if (!is_array($replacements)) {
[136]232            $app->logMsg(sprintf('Cannot replace variables, invalid replacements.', null), LOG_ERR, __FILE__, __LINE__);
[42]233            return false;
[23]234        }
[42]235
[23]236        // Apply regex pattern to search elements.
237        $search = array_keys($replacements);
[247]238        array_walk($search, create_function('&$v', '$v = "{" . mb_strtoupper($v) . "}";'));
[23]239
[35]240        // Replacement values.
[23]241        $replace = array_values($replacements);
[42]242
[24]243        // Search and replace all values at once.
[216]244        $this->_template_replaced = str_replace($search, $replace, $this->_template);
[23]245    }
246
[502]247    /*
248    * Returns the body of the current email. This can be used to store the message that is being sent.
249    * It will use the original template, or the replaced template if it has been processed.
250    *
251    * @access   public
252    * @return   string  Message body.
253    * @author   Quinn Comendant <quinn@strangecode.com>
254    * @version  1.0
255    * @since    18 Nov 2014 21:15:19
256    */
257    public function getBody()
258    {
259        $final_body = isset($this->_template_replaced) ? $this->_template_replaced : $this->_template;
260        // Ensure all placeholders have been replaced. Find anything with {...} characters.
261        if (preg_match('/({[^}]+})/', $final_body, $unreplaced_match)) {
262            unset($unreplaced_match[0]);
263            $app->logMsg(sprintf('Cannot get email body. Unreplaced variables in template: %s', getDump($unreplaced_match)), LOG_ERR, __FILE__, __LINE__);
264            return false;
265        }
266        return $final_body;
267    }
268
[23]269    /**
270     * Send email using PHP's mail() function.
271     *
272     * @access  public
273     * @param   string  $to
274     * @param   string  $from
275     * @param   string  $subject
276     * @author  Quinn Comendant <quinn@strangecode.com>
277     * @since   28 Nov 2005 12:56:09
278     */
[468]279    public function send($to=null, $from=null, $subject=null, $headers=null)
[23]280    {
[479]281        $app =& App::getInstance();
[282]282
[24]283        // Use arguments if provided.
[23]284        if (isset($to)) {
[490]285            $this->setParam(array('to' => $to));
[23]286        }
287        if (isset($from)) {
[490]288            $this->setParam(array('from' => $from));
[23]289        }
290        if (isset($subject)) {
[490]291            $this->setParam(array('subject' => $subject));
[23]292        }
[38]293        if (isset($headers)) {
[490]294            $this->setParam(array('headers' => $headers));
[38]295        }
[36]296
[24]297        // Ensure required values exist.
[43]298        if (!isset($this->_params['subject'])) {
[364]299            $app->logMsg('Cannot send email. SUBJECT not defined.', LOG_ERR, __FILE__, __LINE__);
[23]300            return false;
[43]301        } else if (!isset($this->_template)) {
[136]302            $app->logMsg(sprintf('Cannot send email: "%s". Template not set.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
[43]303            return false;
[23]304        } else if (!isset($this->_params['to'])) {
[136]305            $app->logMsg(sprintf('Cannot send email: "%s". TO not defined.', $this->_params['subject']), LOG_NOTICE, __FILE__, __LINE__);
[23]306            return false;
307        } else if (!isset($this->_params['from'])) {
[136]308            $app->logMsg(sprintf('Cannot send email: "%s". FROM not defined.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
[23]309            return false;
310        }
311
[24]312        // Wrap email text body, using _template_replaced if replacements have been used, or just a fresh _template if not.
[212]313        $final_body = isset($this->_template_replaced) ? $this->_template_replaced : $this->_template;
314        if (false !== $this->getParam('wrap')) {
[454]315            $final_body = wordwrap($final_body, $this->getParam('line_length'), $this->getParam('crlf'));
[212]316        }
[24]317
318        // Ensure all placeholders have been replaced. Find anything with {...} characters.
[23]319        if (preg_match('/({[^}]+})/', $final_body, $unreplaced_match)) {
[502]320            unset($unreplaced_match[0]);
321            $app->logMsg(sprintf('Cannot send email. Unreplaced variables in template: %s', getDump($unreplaced_match)), LOG_ERR, __FILE__, __LINE__);
[23]322            return false;
323        }
[42]324
[24]325        // Final "to" header can have multiple addresses if in an array.
[23]326        $final_to = is_array($this->_params['to']) ? join(', ', $this->_params['to']) : $this->_params['to'];
[42]327
[24]328        // From headers are custom headers.
[38]329        $headers = array('From' => $this->_params['from']);
330
331        // Additional headers.
332        if (isset($this->_params['headers']) && is_array($this->_params['headers'])) {
333            $headers = array_merge($this->_params['headers'], $headers);
334        }
[42]335
[38]336        // Process headers.
337        $final_headers = array();
338        foreach ($headers as $key => $val) {
[490]339            // Validate key and values.
[500]340            if (empty($val)) {
341                $app->logMsg(sprintf('Empty email header provided: %s', $key), LOG_DEBUG, __FILE__, __LINE__);
[490]342                continue;
[424]343            }
[500]344            if (empty($key) || !is_string($key) || !is_string($val) || preg_match("/[\n\r]/", $key . $val) || preg_match('/[^\w-]/', $key)) {
345                $app->logMsg(sprintf('Broken email header provided: %s=%s', $key, $val), LOG_WARNING, __FILE__, __LINE__);
346                continue;
347            }
[490]348            // If the envelope_sender_address was given as a header, move it to the correct place.
349            if ('envelope_sender_address' == $key) {
350                $this->_params['envelope_sender_address'] = isset($this->_params['envelope_sender_address']) ? $this->_params['envelope_sender_address'] : $val;
351                continue;
352            }
[38]353            $final_headers[] = sprintf('%s: %s', $key, $val);
354        }
[212]355        $final_headers = join($this->getParam('crlf'), $final_headers);
[42]356
[24]357        // This is the address where delivery problems are sent to. We must strip off everything except the local@domain part.
[490]358        if (isset($this->_params['envelope_sender_address'])) {
359            $envelope_sender_address = sprintf('<%s>', trim($this->_params['envelope_sender_address'], '<>'));
360        } else {
361            $envelope_sender_address = preg_replace('/^.*<?([^\s@\[\]<>()]+\@[A-Za-z0-9.-]{1,}\.[A-Za-z]{2,5})>?$/iU', '$1', $this->_params['from']);
362        }
[212]363        if ('' != $envelope_sender_address && $this->validEmail($envelope_sender_address)) {
[490]364            $additional_parameter = sprintf('-f %s', $envelope_sender_address);
[212]365        } else {
[490]366            $additional_parameter = '';
[212]367        }
[42]368
[36]369        // Check for mail header injection attacks.
[212]370        $full_mail_content = join($this->getParam('crlf'), array($final_to, $this->_params['subject'], $final_body));
[293]371        if (preg_match("/(^|[\n\r])(Content-Type|MIME-Version|Content-Transfer-Encoding|Bcc|Cc)\s*:/i", $full_mail_content)) {
[136]372            $app->logMsg(sprintf('Mail header injection attack in content: %s', $full_mail_content), LOG_WARNING, __FILE__, __LINE__);
[36]373            return false;
374        }
[346]375
[454]376        // Send email without 5th parameter if safemode is enabled.
[345]377        if (ini_get('safe_mode')) {
[346]378            $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers);
[479]379        } else {
[490]380            $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers, $additional_parameter);
[347]381        }
[454]382
[346]383        // Ensure message was successfully accepted for delivery.
384        if ($ret) {
385            $app->logMsg(sprintf('Email successfully sent to %s', $final_to), LOG_INFO, __FILE__, __LINE__);
386            return true;
387        } else {
[490]388            $app->logMsg(sprintf('Email failure: %s, %s, %s, %s', $final_to, $this->_params['subject'], str_replace("\r\n", '\r\n', $final_headers), $additional_parameter), LOG_WARNING, __FILE__, __LINE__);
[346]389            return false;
[454]390        }
[23]391    }
[42]392
[23]393    /**
394     * Validates an email address based on the recommendations in RFC 3696.
[42]395     * Is more loose than restrictive, to allow the many valid variants of
[23]396     * email addresses while catching the most common mistakes. Checks an array too.
397     * http://www.faqs.org/rfcs/rfc822.html
398     * http://www.faqs.org/rfcs/rfc2822.html
399     * http://www.faqs.org/rfcs/rfc3696.html
400     * http://www.faqs.org/rfcs/rfc1035.html
401     *
402     * @access  public
403     * @param   mixed  $email  Address to check, string or array.
404     * @return  bool    Validity of address.
405     * @author  Quinn Comendant <quinn@strangecode.com>
406     * @since   30 Nov 2005 22:00:50
407     */
[468]408    public function validEmail($email)
[23]409    {
[479]410        $app =& App::getInstance();
[454]411
[24]412        // If an array, check values recursively.
[23]413        if (is_array($email)) {
414            foreach ($email as $e) {
415                if (!$this->validEmail($e)) {
416                    return false;
417                }
418            }
419            return true;
420        } else {
[334]421            // To be valid email address must match regex and fit within the length constraints.
[247]422            if (preg_match($this->getParam('regex'), $email, $e_parts) && mb_strlen($e_parts[2]) < 64 && mb_strlen($e_parts[3]) < 255) {
[24]423                return true;
424            } else {
[394]425                $app->logMsg(sprintf('Invalid email address: %s', $email), LOG_INFO, __FILE__, __LINE__);
[23]426                return false;
427            }
428        }
429    }
430}
431
Note: See TracBrowser for help on using the repository browser.