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

Last change on this file since 550 was 550, checked in by anonymous, 8 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
Line 
1<?php
2/**
3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
5 * Copyright 2001-2012 Strangecode, LLC
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/*
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.
34$email = new Email(array(
35    'to' => array($frm['email'], 'q@lovemachine.local'),
36    'from' => sprintf('"%s" <%s>', addcslashes($app->getParam('site_name'), '"'), $app->getParam('site_email')),
37    'subject' => 'Your account has been activated',
38));
39$email->setTemplate('email_registration_confirm.ihtml');
40// $email->setString('Or you can pass your message body as a string, also with {VARIABLES}.');
41$email->replace(array(
42    'site_name' => $app->getParam('site_name'),
43    'site_url' => $app->getParam('site_url'),
44    'username' => $frm['username'],
45    'password' => $frm['password1'],
46));
47if ($email->send()) {
48    $app->raiseMsg(sprintf(_("A confirmation email has been sent to %s."), $frm['email']), MSG_SUCCESS, __FILE__, __LINE__);
49} else {
50    $app->logMsg(sprintf('Error sending confirmation email to address %s', $frm['email']), LOG_NOTICE, __FILE__, __LINE__);
51}
52---------------------------------------------------------------------
53*/
54
55class Email
56{
57
58    // Default parameters, to be overwritten by setParam() and read with getParam()
59    protected $_params = array(
60        'to' => null,
61        'from' => null,
62        'subject' => null,
63        'headers' => null,
64        'envelope_sender_address' => null, // AKA the bounce-to address. Will default to 'from' if left null.
65        'regex' => null,
66
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",
70
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,
75    );
76
77    // String that contains the email body.
78    protected $_template;
79
80    // String that contains the email body after replacements.
81    protected $_template_replaced;
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     */
91    public function __construct($params=null)
92    {
93        // The regex used in validEmail(). Set here instead of in the default _params above so we can use the concatenation . dot.
94        // This matches a (valid) email address as complex as:
95        //      "Jane & Bob Smith" <bob&smith's/dep=sales!@smith-wick.ca.us > (Sales department)
96        // ...and something as simple as:
97        //      x@x.com
98        $this->setParam(array('regex' => '/^(?:(?:"[^"]*?"\s*|[^,@]*)(<\s*)|(?:"[^"]*?"|[^,@]*)\s+|)'   // Display name
99        . '((?:[^.<>\s@",\[\]]+[^<>\s@",\[\]])*[^.<>\s@",\[\]]+)'       // Local-part
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
107        . '(?:\s*>\s*|>\s+\([^,@]+\)\s*)'                               // TRUE, ensure ending >
108        . '|'
109        . '(?:|\s*|\s+\([^,@]+\)\s*))$/i'));                            // FALSE ensure there is no ending >
110
111        if (isset($params)) {
112            $this->setParam($params);
113        }
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     */
122    public function setParam($params)
123    {
124        $app =& App::getInstance();
125
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            }
134            if (isset($params['envelope_sender_address']) && !$this->validEmail($params['envelope_sender_address'])) {
135                $params['envelope_sender_address'] = null;
136            }
137
138            // Merge new parameters with old overriding only those passed.
139            $this->_params = array_merge($this->_params, $params);
140        } else {
141            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
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     */
152    public function getParam($param)
153    {
154        $app =& App::getInstance();
155
156        if (array_key_exists($param, $this->_params)) {
157            return $this->_params[$param];
158        } else {
159            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
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     */
172    public function setTemplate($template)
173    {
174        $app =& App::getInstance();
175
176        // Load file, using include_path.
177        if (!$this->_template = file_get_contents($template, true)) {
178            $app->logMsg(sprintf('Email template file does not exist: %s', $template), LOG_ERR, __FILE__, __LINE__);
179            $this->_template = null;
180            $this->_template_replaced = null;
181            return false;
182        }
183        // This could be a new template, so reset the _template_replaced.
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     */
196    public function setString($string)
197    {
198        $app =& App::getInstance();
199
200        if ('' == trim($string)) {
201            $app->logMsg(sprintf('Empty string provided.', null), LOG_ERR, __FILE__, __LINE__);
202            $this->_template_replaced = null;
203            return false;
204        } else {
205            $this->_template = $string;
206            // This could be a new template, so reset the _template_replaced.
207            $this->_template_replaced = null;
208            return true;
209        }
210    }
211
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     */
220    public function replace($replacements)
221    {
222        $app =& App::getInstance();
223
224        // Ensure template exists.
225        if (!isset($this->_template)) {
226            $app->logMsg(sprintf('Cannot replace variables, no template defined.', null), LOG_ERR, __FILE__, __LINE__);
227            return false;
228        }
229
230        // Ensure replacements argument is an array.
231        if (!is_array($replacements)) {
232            $app->logMsg(sprintf('Cannot replace variables, invalid replacements.', null), LOG_ERR, __FILE__, __LINE__);
233            return false;
234        }
235
236        // Apply regex pattern to search elements.
237        $search = array_keys($replacements);
238        array_walk($search, create_function('&$v', '$v = "{" . mb_strtoupper($v) . "}";'));
239
240        // Replacement values.
241        $replace = array_values($replacements);
242
243        // Search and replace all values at once.
244        $this->_template_replaced = str_replace($search, $replace, $this->_template);
245    }
246
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
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     */
279    public function send($to=null, $from=null, $subject=null, $headers=null)
280    {
281        $app =& App::getInstance();
282
283        // Use arguments if provided.
284        if (isset($to)) {
285            $this->setParam(array('to' => $to));
286        }
287        if (isset($from)) {
288            $this->setParam(array('from' => $from));
289        }
290        if (isset($subject)) {
291            $this->setParam(array('subject' => $subject));
292        }
293        if (isset($headers)) {
294            $this->setParam(array('headers' => $headers));
295        }
296
297        // Ensure required values exist.
298        if (!isset($this->_params['subject'])) {
299            $app->logMsg('Cannot send email. SUBJECT not defined.', LOG_ERR, __FILE__, __LINE__);
300            return false;
301        } else if (!isset($this->_template)) {
302            $app->logMsg(sprintf('Cannot send email: "%s". Template not set.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
303            return false;
304        } else if (!isset($this->_params['to'])) {
305            $app->logMsg(sprintf('Cannot send email: "%s". TO not defined.', $this->_params['subject']), LOG_NOTICE, __FILE__, __LINE__);
306            return false;
307        } else if (!isset($this->_params['from'])) {
308            $app->logMsg(sprintf('Cannot send email: "%s". FROM not defined.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
309            return false;
310        }
311
312        // Wrap email text body, using _template_replaced if replacements have been used, or just a fresh _template if not.
313        $final_body = isset($this->_template_replaced) ? $this->_template_replaced : $this->_template;
314        if (false !== $this->getParam('wrap')) {
315            $final_body = wordwrap($final_body, $this->getParam('line_length'), $this->getParam('crlf'));
316        }
317
318        // Ensure all placeholders have been replaced. Find anything with {...} characters.
319        if (preg_match('/({[^}]+})/', $final_body, $unreplaced_match)) {
320            unset($unreplaced_match[0]);
321            $app->logMsg(sprintf('Cannot send email. Unreplaced variables in template: %s', getDump($unreplaced_match)), LOG_ERR, __FILE__, __LINE__);
322            return false;
323        }
324
325        // Final "to" header can have multiple addresses if in an array.
326        $final_to = is_array($this->_params['to']) ? join(', ', $this->_params['to']) : $this->_params['to'];
327
328        // From headers are custom headers.
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        }
335
336        // Process headers.
337        $final_headers = array();
338        foreach ($headers as $key => $val) {
339            // Validate key and values.
340            if (empty($val)) {
341                $app->logMsg(sprintf('Empty email header provided: %s', $key), LOG_DEBUG, __FILE__, __LINE__);
342                continue;
343            }
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            }
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            }
353            $final_headers[] = sprintf('%s: %s', $key, $val);
354        }
355        $final_headers = join($this->getParam('crlf'), $final_headers);
356
357        // This is the address where delivery problems are sent to. We must strip off everything except the local@domain part.
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        }
363        if ('' != $envelope_sender_address && $this->validEmail($envelope_sender_address)) {
364            $additional_parameter = sprintf('-f %s', $envelope_sender_address);
365        } else {
366            $additional_parameter = '';
367        }
368
369        // Check for mail header injection attacks.
370        $full_mail_content = join($this->getParam('crlf'), array($final_to, $this->_params['subject'], $final_body));
371        if (preg_match("/(^|[\n\r])(Content-Type|MIME-Version|Content-Transfer-Encoding|Bcc|Cc)\s*:/i", $full_mail_content)) {
372            $app->logMsg(sprintf('Mail header injection attack in content: %s', $full_mail_content), LOG_WARNING, __FILE__, __LINE__);
373            return false;
374        }
375
376        // Send email without 5th parameter if safemode is enabled.
377        if (ini_get('safe_mode')) {
378            $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers);
379        } else {
380            $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers, $additional_parameter);
381        }
382
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 {
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__);
389            return false;
390        }
391    }
392
393    /**
394     * Validates an email address based on the recommendations in RFC 3696.
395     * Is more loose than restrictive, to allow the many valid variants of
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     */
408    public function validEmail($email)
409    {
410        $app =& App::getInstance();
411
412        // If an array, check values recursively.
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 {
421            // To be valid email address must match regex and fit within the length constraints.
422            if (preg_match($this->getParam('regex'), $email, $e_parts) && mb_strlen($e_parts[2]) < 64 && mb_strlen($e_parts[3]) < 255) {
423                return true;
424            } else {
425                $app->logMsg(sprintf('Invalid email address: %s', $email), LOG_INFO, __FILE__, __LINE__);
426                return false;
427            }
428        }
429    }
430}
431
Note: See TracBrowser for help on using the repository browser.