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

Last change on this file since 357 was 357, checked in by quinn, 15 years ago

updated tests to work. updated email validation regex to include quote marks around name part. changed logmsg tmp dir name to use script_filename.

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