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

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

${1}

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