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

Last change on this file since 424 was 424, checked in by anonymous, 11 years ago

Added error checking for invalid email header arguments; cleaned up HTML in lock message.

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