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

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

Many minor fixes during pulso development

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>', $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.