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

Last change on this file since 146 was 146, checked in by scdev, 18 years ago

Q - added persistant database storage to Prefs.inc.php. Modified getParam failure log type to LOG_DEBUG in all classes.

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