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

Last change on this file since 501 was 500, checked in by anonymous, 10 years ago

Many auth and crypto changes; various other bugfixes while working on pulso.

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