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

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

Edit comments

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