source: branches/eli_branch/lib/Email.inc.php @ 450

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

added public and private keywords to all properties and methods, changed old classname constructor function to construct, removed more ?> closing tags

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 
32// Example.
33$email = new Email(array(
34    'to' => array($frm['email'], 'q@lovemachine.local'),
35    'from' => sprintf('%s <%s>', $app->getParam('site_name'), $app->getParam('site_email')),
36    'subject' => 'Your account has been activated',
37));
38$email->setTemplate('email_registration_confirm.ihtml');
39// $email->setString('Or you can pass your message body as a string, also with {VARIABLES}.');
40$email->replace(array(
41    'site_name' => $app->getParam('site_name'),
42    'site_url' => $app->getParam('site_url'),
43    'username' => $frm['username'],
44    'password' => $frm['password1'],
45));
46if ($email->send()) {
47    $app->raiseMsg(sprintf(_("A confirmation email has been sent to %s."), $frm['email']), MSG_SUCCESS, __FILE__, __LINE__);
48} else {
49    $app->logMsg(sprintf('Error sending confirmation email to address %s', $frm['email']), LOG_NOTICE, __FILE__, __LINE__);
50}
51
52 *
53 */
54class Email {
55
56    // Default parameters, to be overwritten by setParam() and read with getParam()
57    private $_params = array(
58        'to' => null,
59        'from' => null,
60        'subject' => null,
61        'headers' => null,
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,
72    );
73
74    // String that contains the email body.
75    private $_template;
76
77    // String that contains the email body after replacements.
78    private $_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    public function __construct($params=null)
89    {
90        // The regex used in validEmail(). Set here instead of in the default _params above so we can use the concatenation . dot.
91        // This matches a (valid) email address as complex as:
92        //      "Jane & Bob Smith" <bob&smith's/dep=sales!@smith-wick.ca.us > (Sales department)
93        // ...and something as simple as:
94        //      x@x.com
95        $this->setParam(array('regex' => '/^(?:(?:"[^"]*?"\s*|[^,@]*)(<\s*)|(?:"[^"]*?"|[^,@]*)\s+|)'   // Display name
96        . '((?:[^.<>\s@",\[\]]+[^<>\s@",\[\]])*[^.<>\s@",\[\]]+)'       // Local-part
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
104        . '(?:\s*>\s*|>\s+\([^,@]+\)\s*)'                                  // TRUE, ensure ending >
105        . '|'
106        . '(?:|\s*|\s+\([^,@]+\)\s*))$/i'));                            // FALSE ensure there is no ending >
107
108        if (isset($params)) {
109            $this->setParam($params);
110        }
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    public function setParam($params)
120    {
121        $app =& App::getInstance();
122   
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 {
135            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
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    public function getParam($param)
147    {
148        $app =& App::getInstance();
149   
150        if (isset($this->_params[$param])) {
151            return $this->_params[$param];
152        } else {
153            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
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    public function setTemplate($template)
167    {
168        $app =& App::getInstance();
169   
170        // Load file, using include_path.
171        if (!$this->_template = file_get_contents($template, true)) {
172            $app->logMsg(sprintf('Email template file does not exist: %s', $template), LOG_ERR, __FILE__, __LINE__);
173            $this->_template = null;
174            $this->_template_replaced = null;
175            return false;
176        }
177        // This could be a new template, so reset the _template_replaced.
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    public function setString($string)
191    {
192        $app =& App::getInstance();
193   
194        if ('' == trim($string)) {
195            $app->logMsg(sprintf('Empty string provided.', null), LOG_ERR, __FILE__, __LINE__);
196            $this->_template_replaced = null;
197            return false;
198        } else {
199            $this->_template = $string;
200            // This could be a new template, so reset the _template_replaced.
201            $this->_template_replaced = null;
202            return true;
203        }
204    }
205
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    public function replace($replacements)
215    {
216        $app =& App::getInstance();
217   
218        // Ensure template exists.
219        if (!isset($this->_template)) {
220            $app->logMsg(sprintf('Cannot replace variables, no template defined.', null), LOG_ERR, __FILE__, __LINE__);
221            return false;
222        }
223
224        // Ensure replacements argument is an array.
225        if (!is_array($replacements)) {
226            $app->logMsg(sprintf('Cannot replace variables, invalid replacements.', null), LOG_ERR, __FILE__, __LINE__);
227            return false;
228        }
229
230        // Apply regex pattern to search elements.
231        $search = array_keys($replacements);
232        array_walk($search, create_function('&$v', '$v = "{" . mb_strtoupper($v) . "}";'));
233
234        // Replacement values.
235        $replace = array_values($replacements);
236
237        // Search and replace all values at once.
238        $this->_template_replaced = str_replace($search, $replace, $this->_template);
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     */
251    public function send($to=null, $from=null, $subject=null, $headers=null)
252    {
253        $app =& App::getInstance();
254
255        // Use arguments if provided.
256        if (isset($to)) {
257             $this->setParam(array('to' => $to));
258        }
259        if (isset($from)) {
260             $this->setParam(array('from' => $from));
261        }
262        if (isset($subject)) {
263             $this->setParam(array('subject' => $subject));
264        }
265        if (isset($headers)) {
266             $this->setParam(array('headers' => $headers));
267        }
268
269        // Ensure required values exist.
270        if (!isset($this->_params['subject'])) {
271            $app->logMsg('Cannot send email. SUBJECT not defined.', LOG_ERR, __FILE__, __LINE__);
272            return false;
273        } else if (!isset($this->_template)) {
274            $app->logMsg(sprintf('Cannot send email: "%s". Template not set.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
275            return false;
276        } else if (!isset($this->_params['to'])) {
277            $app->logMsg(sprintf('Cannot send email: "%s". TO not defined.', $this->_params['subject']), LOG_NOTICE, __FILE__, __LINE__);
278            return false;
279        } else if (!isset($this->_params['from'])) {
280            $app->logMsg(sprintf('Cannot send email: "%s". FROM not defined.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
281            return false;
282        }
283
284        // Wrap email text body, using _template_replaced if replacements have been used, or just a fresh _template if not.
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        }
289
290        // Ensure all placeholders have been replaced. Find anything with {...} characters.
291        if (preg_match('/({[^}]+})/', $final_body, $unreplaced_match)) {
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__);
293            return false;
294        }
295
296        // Final "to" header can have multiple addresses if in an array.
297        $final_to = is_array($this->_params['to']) ? join(', ', $this->_params['to']) : $this->_params['to'];
298
299        // From headers are custom headers.
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        }
306
307        // Process headers.
308        $final_headers = array();
309        foreach ($headers as $key => $val) {
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            }
313            $final_headers[] = sprintf('%s: %s', $key, $val);
314        }
315        $final_headers = join($this->getParam('crlf'), $final_headers);
316
317        // This is the address where delivery problems are sent to. We must strip off everything except the local@domain part.
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        }
324
325        // Check for mail header injection attacks.
326        $full_mail_content = join($this->getParam('crlf'), array($final_to, $this->_params['subject'], $final_body));
327        if (preg_match("/(^|[\n\r])(Content-Type|MIME-Version|Content-Transfer-Encoding|Bcc|Cc)\s*:/i", $full_mail_content)) {
328            $app->logMsg(sprintf('Mail header injection attack in content: %s', $full_mail_content), LOG_WARNING, __FILE__, __LINE__);
329            sleep(3);
330            return false;
331        }
332
333        // Send email without 5th parameter if safemode is enabled.
334        if (ini_get('safe_mode')) {
335            $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers);
336        } else {
337            $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers, $envelope_sender_header);
338        }
339       
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        } 
348    }
349
350    /**
351     * Validates an email address based on the recommendations in RFC 3696.
352     * Is more loose than restrictive, to allow the many valid variants of
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    public function validEmail($email)
366    {
367        $app =& App::getInstance();
368   
369        // If an array, check values recursively.
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 {
378            // To be valid email address must match regex and fit within the length constraints.
379            if (preg_match($this->getParam('regex'), $email, $e_parts) && mb_strlen($e_parts[2]) < 64 && mb_strlen($e_parts[3]) < 255) {
380                return true;
381            } else {
382                $app->logMsg(sprintf('Invalid email address: %s', $email), LOG_INFO, __FILE__, __LINE__);
383                return false;
384            }
385        }
386    }
387}
388
Note: See TracBrowser for help on using the repository browser.