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

Last change on this file since 212 was 212, checked in by scdev, 17 years ago

Q - added config for line endings and line length.

File size: 14.1 KB
RevLine 
[23]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
[136]10 *
11 
[23]12// Example.
13$email = new Email(array(
14    'to' => array($frm['email'], 'q@lovemachine.local'),
[136]15    'from' => sprintf('%s <%s>', $app->getParam('site_name'), $app->getParam('site_email')),
[24]16    'subject' => 'Your account has been activated',
[23]17));
18$email->setTemplate('email_registration_confirm.ihtml');
[41]19// $email->setString('Or you can pass your message body as a string, also with {VARIABLES}.');
[23]20$email->replace(array(
[136]21    'site_name' => $app->getParam('site_name'),
22    'site_url' => $app->getParam('site_url'),
[23]23    'username' => $frm['username'],
24    'password' => $frm['password1'],
25));
26if ($email->send()) {
[136]27    $app->raiseMsg(sprintf(_("A confirmation email has been sent to %s."), $frm['email']), MSG_SUCCESS, __FILE__, __LINE__);
[23]28} else {
[136]29    $app->logMsg(sprintf('Error sending confirmation email to address %s', $frm['email']), LOG_NOTICE, __FILE__, __LINE__);
[23]30}
[136]31
32 *
[23]33 */
34class Email {
35
[24]36    // Default parameters, to be overwritten by setParam() and read with getParam()
[23]37    var $_params = array(
38        'to' => null,
39        'from' => null,
40        'subject' => null,
[38]41        'headers' => null,
[212]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,
[23]52    );
[42]53
[24]54    // String that contains the email body.
[23]55    var $_template;
[42]56
[24]57    // String that contains the email body after replacements.
[23]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    {
[24]70        // The regex used in validEmail(). Set here instead of in the default _params above so we can use the concatination . dot.
[42]71        // This matches an email address as complex as:
[24]72        //      Bob Smith <bob&smith's/dep=sales!@smith-wick.ca.us> (Sales department)
73        // ...and something as simple as:
74        //      x@x.com
[23]75        $this->setParam(array('regex' => '/^(?:[^,@]*\s+|[^,@]*(<)|)'   // Display name
[42]76        . '((?:[^.<>\s@\",\[\]]+[^<>\s@\",\[\]])*[^.<>\s@\",\[\]]+)'    // Local-part
[23]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
[25]88        if (isset($params)) {
89            $this->setParam($params);
90        }
[23]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    {
[136]101        $app =& App::getInstance();
102   
[23]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 {
[136]115            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
[23]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    {
[136]128        $app =& App::getInstance();
129   
[23]130        if (isset($this->_params[$param])) {
131            return $this->_params[$param];
132        } else {
[146]133            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
[23]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    {
[136]148        $app =& App::getInstance();
149   
[23]150        // Load file, using include_path.
151        if (!$this->_template = file_get_contents($template, true)) {
[136]152            $app->logMsg(sprintf('Email template file does not exist: %s', $template), LOG_ERR, __FILE__, __LINE__);
[23]153            $this->_template = null;
154            $this->_template_replaced = null;
155            return false;
156        }
[24]157        // This could be a new template, so reset the _template_replaced.
[23]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    {
[136]172        $app =& App::getInstance();
173   
[23]174        // Load file, using include_path.
[41]175        if ('' == trim($string)) {
[136]176            $app->logMsg(sprintf('Empty string provided.', null), LOG_ERR, __FILE__, __LINE__);
[23]177            $this->_template_replaced = null;
178            return false;
179        } else {
[41]180            $this->_template = $string;
[24]181            // This could be a new template, so reset the _template_replaced.
[23]182            $this->_template_replaced = null;
183            return true;
184        }
185    }
[42]186
[23]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    {
[136]197        $app =& App::getInstance();
198   
[24]199        // Ensure template exists.
[23]200        if (!isset($this->_template)) {
[136]201            $app->logMsg(sprintf('Cannot replace variables, no template defined.', null), LOG_ERR, __FILE__, __LINE__);
[42]202            return false;
[23]203        }
[42]204
[24]205        // Ensure replacements argument is an array.
[23]206        if (!is_array($replacements)) {
[136]207            $app->logMsg(sprintf('Cannot replace variables, invalid replacements.', null), LOG_ERR, __FILE__, __LINE__);
[42]208            return false;
[23]209        }
[42]210
[23]211        // Apply regex pattern to search elements.
212        $search = array_keys($replacements);
213        array_walk($search, create_function('&$v', '$v = "/{" . preg_quote($v) . "}/i";'));
214
[35]215        // Replacement values.
[23]216        $replace = array_values($replacements);
[42]217
[24]218        // Search and replace all values at once.
[23]219        $this->_template_replaced = preg_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     */
[38]232    function send($to=null, $from=null, $subject=null, $headers=null)
[23]233    {
[136]234        $app =& App::getInstance();
235   
[24]236        // Use arguments if provided.
[23]237        if (isset($to)) {
[42]238             $this->setParam(array('to' => $to));
[23]239        }
240        if (isset($from)) {
[42]241             $this->setParam(array('from' => $from));
[23]242        }
243        if (isset($subject)) {
[42]244             $this->setParam(array('subject' => $subject));
[23]245        }
[38]246        if (isset($headers)) {
[42]247             $this->setParam(array('headers' => $headers));
[38]248        }
[36]249
[24]250        // Ensure required values exist.
[43]251        if (!isset($this->_params['subject'])) {
[136]252            $app->logMsg(sprintf('Cannot send email to %s. SUBJECT not defined.', $this->_params['to']), LOG_ERR, __FILE__, __LINE__);
[23]253            return false;
[43]254        } else if (!isset($this->_template)) {
[136]255            $app->logMsg(sprintf('Cannot send email: "%s". Template not set.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
[43]256            return false;
[23]257        } else if (!isset($this->_params['to'])) {
[136]258            $app->logMsg(sprintf('Cannot send email: "%s". TO not defined.', $this->_params['subject']), LOG_NOTICE, __FILE__, __LINE__);
[23]259            return false;
260        } else if (!isset($this->_params['from'])) {
[136]261            $app->logMsg(sprintf('Cannot send email: "%s". FROM not defined.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
[23]262            return false;
263        }
264
[24]265        // Wrap email text body, using _template_replaced if replacements have been used, or just a fresh _template if not.
[212]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        }
[24]270
271        // Ensure all placeholders have been replaced. Find anything with {...} characters.
[23]272        if (preg_match('/({[^}]+})/', $final_body, $unreplaced_match)) {
[136]273            $app->logMsg(sprintf('Cannot send email. Variables left unreplaced in template: %s', (isset($unreplaced_match[1]) ? $unreplaced_match[1] : '')), LOG_ERR, __FILE__, __LINE__);
[23]274            return false;
275        }
[42]276
[24]277        // Final "to" header can have multiple addresses if in an array.
[23]278        $final_to = is_array($this->_params['to']) ? join(', ', $this->_params['to']) : $this->_params['to'];
[42]279
[24]280        // From headers are custom headers.
[38]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        }
[42]287
[38]288        // Process headers.
289        $final_headers = array();
290        foreach ($headers as $key => $val) {
291            $final_headers[] = sprintf('%s: %s', $key, $val);
292        }
[212]293        $final_headers = join($this->getParam('crlf'), $final_headers);
[42]294
[24]295        // This is the address where delivery problems are sent to. We must strip off everything except the local@domain part.
[212]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        }
[42]302
[36]303        // Check for mail header injection attacks.
[212]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):/i", $full_mail_content)) {
[136]306            $app->logMsg(sprintf('Mail header injection attack in content: %s', $full_mail_content), LOG_WARNING, __FILE__, __LINE__);
[42]307            sleep(3);
[36]308            return false;
309        }
[24]310
311        // Ensure message was successfully accepted for delivery.
[114]312        if (mail($final_to, $this->_params['subject'], $final_body, $final_headers, $envelope_sender_header)) {
[201]313            $app->logMsg(sprintf('Email successfully sent to %s', $final_to), LOG_INFO, __FILE__, __LINE__);
[119]314            return true;
[114]315        } else {
[136]316            $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__);
[23]317            return false;
318        }
319    }
[42]320
[23]321    /**
322     * Validates an email address based on the recommendations in RFC 3696.
[42]323     * Is more loose than restrictive, to allow the many valid variants of
[23]324     * email addresses while catching the most common mistakes. Checks an array too.
325     * http://www.faqs.org/rfcs/rfc822.html
326     * http://www.faqs.org/rfcs/rfc2822.html
327     * http://www.faqs.org/rfcs/rfc3696.html
328     * http://www.faqs.org/rfcs/rfc1035.html
329     *
330     * @access  public
331     * @param   mixed  $email  Address to check, string or array.
332     * @return  bool    Validity of address.
333     * @author  Quinn Comendant <quinn@strangecode.com>
334     * @since   30 Nov 2005 22:00:50
335     */
336    function validEmail($email)
337    {
[136]338        $app =& App::getInstance();
339   
[24]340        // If an array, check values recursively.
[23]341        if (is_array($email)) {
342            foreach ($email as $e) {
343                if (!$this->validEmail($e)) {
344                    return false;
345                }
346            }
347            return true;
348        } else {
[24]349            // To be valid email address must match regex and fit within the lenth constraints.
350            if (preg_match($this->getParam('regex'), $email, $e_parts) && strlen($e_parts[2]) < 64 && strlen($e_parts[3]) < 255) {
351                return true;
352            } else {
[136]353                $app->logMsg(sprintf('Invalid email: %s', $email), LOG_INFO, __FILE__, __LINE__);
[23]354                return false;
355            }
356        }
357    }
358}
359
360?>
Note: See TracBrowser for help on using the repository browser.