Index: branches/1.1dev/lib/Email.inc.php
===================================================================
--- branches/1.1dev/lib/Email.inc.php	(revision 292)
+++ branches/1.1dev/lib/Email.inc.php	(revision 292)
@@ -0,0 +1,347 @@
+<?php
+/**
+ * Email.inc.php
+ * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
+ *
+ * Easy email template usage.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.0 - backported to codebase v1.1dev
+ * 
+ 
+// Example.
+$email = new Email(array(
+    'to' => array($frm['email'], 'q@lovemachine.local'),
+    'from' => sprintf('%s <%s>', $CFG->site_name, $CFG->site_email),
+    'subject' => 'Your account has been activated',
+));
+$email->setTemplate('email_registration_confirm.ihtml');
+// $email->setString('Or you can pass your message body as a string, also with {VARIABLES}.');
+$email->replace(array(
+    'SITE_NAME' => $CFG->site_name,
+    'SITE_URL' => $CFG->site_url,
+    'USERNAME' => $frm['username'],
+    'PASSWORD' => $frm['password1'],
+));
+if ($email->send()) {
+    raiseMsg(sprintf(_("A confirmation email has been sent to %s."), $frm['email']), MSG_SUCCESS, __FILE__, __LINE__);
+} else {
+    logMsg(sprintf('Error sending confirmation email to address %s', $frm['email']), LOG_NOTICE, __FILE__, __LINE__);
+}
+
+ * 
+ */
+class Email {
+
+    // Default parameters, to be overwritten by setParam() and read with getParam()
+    var $_params = array(
+        'to' => null,
+        'from' => null,
+        'subject' => null,
+        'headers' => null,
+        'regex' => null,
+        
+        // A single carriage return (\n) should terminate lines for locally injected mail.
+        // A carriage return + line-feed (\r\n) should be used if sending mail directly with SMTP.
+        'crlf' => "\n",
+        
+        // RFC 2822 says line length MUST be no more than 998 characters, and SHOULD be no more than 78 characters, excluding the CRLF.
+        // http://mailformat.dan.info/body/linelength.html
+        'wrap' => true,
+        'line_length' => 75,
+    );
+
+    // String that contains the email body.
+    var $_template;
+
+    // String that contains the email body after replacements.
+    var $_template_replaced;
+
+    /**
+     * Constructor.
+     *
+     * @access  public
+     * @param   array   $params     Array of object parameters.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Nov 2005 12:59:41
+     */
+    function Email($params=null)
+    {
+        // The regex used in validEmail(). Set here instead of in the default _params above so we can use the concatination . dot.
+        // This matches an email address as complex as:
+        //      Bob Smith <bob&smith's/dep=sales!@smith-wick.ca.us> (Sales department)
+        // ...and something as simple as:
+        //      x@x.com
+        $this->setParam(array('regex' => '/^(?:[^,@]*\s+|[^,@]*(<)|)'   // Display name
+        . '((?:[^.<>\s@\",\[\]]+[^<>\s@\",\[\]])*[^.<>\s@\",\[\]]+)'    // Local-part
+        . '@'                                                           // @
+        . '((?:(\[)|[A-Z0-9]?)'                                         // Domain, first char
+        . '(?(4)'                                                       // Domain conditional for if first domain char is [
+        . '(?:[0-9]{1,3}\.){3}[0-9]{1,3}\]'                             // TRUE, matches IP address
+        . '|'
+        . '[.-]?(?:[A-Z0-9]+[-.])*(?:[A-Z0-9]+\.)+[A-Z]{2,6}))'         // FALSE, matches domain name
+        . '(?(1)'                                                       // Comment conditional for if initial < exists
+        . '(?:>\s*|>\s+\([^,@]+\)\s*)'                                  // TRUE, ensure ending >
+        . '|'
+        . '(?:|\s*|\s+\([^,@]+\)\s*))$/i'));                            // FALSE ensure there is no ending >
+
+        if (isset($params)) {
+            $this->setParam($params);
+        }
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    function setParam($params)
+    {
+        if (isset($params) && is_array($params)) {
+            // Enforce valid email addresses.
+            if (isset($params['to']) && !$this->validEmail($params['to'])) {
+                $params['to'] = null;
+            }
+            if (isset($params['from']) && !$this->validEmail($params['from'])) {
+                $params['from'] = null;
+            }
+
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            return $this->_params[$param];
+        } else {
+            logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Loads template from file to generate email body.
+     *
+     * @access  public
+     * @param   string  $template   Filename of email template.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Nov 2005 12:56:23
+     */
+    function setTemplate($template)
+    {
+        // Load file, using include_path.
+        if (!$this->_template = file_get_contents($template, true)) {
+            logMsg(sprintf('Email template file does not exist: %s', $template), LOG_ERR, __FILE__, __LINE__);
+            $this->_template = null;
+            $this->_template_replaced = null;
+            return false;
+        }
+        // This could be a new template, so reset the _template_replaced.
+        $this->_template_replaced = null;
+        return true;
+    }
+
+    /**
+     * Loads template from string to generate email body.
+     *
+     * @access  public
+     * @param   string  $template   Filename of email template.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Nov 2005 12:56:23
+     */
+    function setString($string)
+    {
+        // Load file, using include_path.
+        if ('' == trim($string)) {
+            logMsg(sprintf('Empty string provided.', null), LOG_ERR, __FILE__, __LINE__);
+            $this->_template_replaced = null;
+            return false;
+        } else {
+            $this->_template = $string;
+            // This could be a new template, so reset the _template_replaced.
+            $this->_template_replaced = null;
+            return true;
+        }
+    }
+
+    /**
+     * Replace variables in template with argument data.
+     *
+     * @access  public
+     * @param   array   $replacements   Array keys are the values to search for, array vales are the replacement values.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Nov 2005 13:08:51
+     */
+    function replace($replacements)
+    {
+    
+        // Ensure template exists.
+        if (!isset($this->_template)) {
+            logMsg(sprintf('Cannot replace variables, no template defined.', null), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure replacements argument is an array.
+        if (!is_array($replacements)) {
+            logMsg(sprintf('Cannot replace variables, invalid replacements.', null), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Apply regex pattern to search elements.
+        $search = array_keys($replacements);
+        array_walk($search, create_function('&$v', '$v = "{" . mb_strtoupper($v) . "}";'));
+
+        // Replacement values.
+        $replace = array_values($replacements);
+
+        // Search and replace all values at once.
+        $this->_template_replaced = str_replace($search, $replace, $this->_template);
+    }
+
+    /**
+     * Send email using PHP's mail() function.
+     *
+     * @access  public
+     * @param   string  $to
+     * @param   string  $from
+     * @param   string  $subject
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Nov 2005 12:56:09
+     */
+    function send($to=null, $from=null, $subject=null, $headers=null)
+    {
+        // Use arguments if provided.
+        if (isset($to)) {
+             $this->setParam(array('to' => $to));
+        }
+        if (isset($from)) {
+             $this->setParam(array('from' => $from));
+        }
+        if (isset($subject)) {
+             $this->setParam(array('subject' => $subject));
+        }
+        if (isset($headers)) {
+             $this->setParam(array('headers' => $headers));
+        }
+
+        // Ensure required values exist.
+        if (!isset($this->_params['subject'])) {
+            logMsg(sprintf('Cannot send email to %s. SUBJECT not defined.', $this->_params['to']), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        } else if (!isset($this->_template)) {
+            logMsg(sprintf('Cannot send email: "%s". Template not set.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        } else if (!isset($this->_params['to'])) {
+            logMsg(sprintf('Cannot send email: "%s". TO not defined.', $this->_params['subject']), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        } else if (!isset($this->_params['from'])) {
+            logMsg(sprintf('Cannot send email: "%s". FROM not defined.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Wrap email text body, using _template_replaced if replacements have been used, or just a fresh _template if not.
+        $final_body = isset($this->_template_replaced) ? $this->_template_replaced : $this->_template;
+        if (false !== $this->getParam('wrap')) {
+            $final_body = wordwrap($final_body, $this->getParam('line_length'), $this->getParam('crlf'));            
+        }
+
+        // Ensure all placeholders have been replaced. Find anything with {...} characters.
+        if (preg_match('/({[^}]+})/', $final_body, $unreplaced_match)) {
+            logMsg(sprintf('Cannot send email. Variables left unreplaced in template: %s', (isset($unreplaced_match[1]) ? $unreplaced_match[1] : '')), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Final "to" header can have multiple addresses if in an array.
+        $final_to = is_array($this->_params['to']) ? join(', ', $this->_params['to']) : $this->_params['to'];
+
+        // From headers are custom headers.
+        $headers = array('From' => $this->_params['from']);
+
+        // Additional headers.
+        if (isset($this->_params['headers']) && is_array($this->_params['headers'])) {
+            $headers = array_merge($this->_params['headers'], $headers);
+        }
+
+        // Process headers.
+        $final_headers = array();
+        foreach ($headers as $key => $val) {
+            $final_headers[] = sprintf('%s: %s', $key, $val);
+        }
+        $final_headers = join($this->getParam('crlf'), $final_headers);
+
+        // This is the address where delivery problems are sent to. We must strip off everything except the local@domain part.
+        $envelope_sender_address = preg_replace('/^.*<?([^\s@\[\]<>()]+\@[A-Za-z0-9.-]{1,}\.[A-Za-z]{2,5})>?$/iU', '$1', $this->_params['from']);
+        if ('' != $envelope_sender_address && $this->validEmail($envelope_sender_address)) {
+            $envelope_sender_header = sprintf('-f %s', $envelope_sender_address);
+        } else {
+            $envelope_sender_header = '';            
+        }
+
+        // Check for mail header injection attacks.
+        $full_mail_content = join($this->getParam('crlf'), array($final_to, $this->_params['subject'], $final_body));
+        if (preg_match("/(^|[\n\r])(Content-Type|MIME-Version|Content-Transfer-Encoding|Bcc|Cc):/i", $full_mail_content)) {
+            logMsg(sprintf('Mail header injection attack in content: %s', $full_mail_content), LOG_WARNING, __FILE__, __LINE__);
+            sleep(3);
+            return false;
+        }
+
+        // Ensure message was successfully accepted for delivery.
+        if (mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers, $envelope_sender_header)) {
+            logMsg(sprintf('Email successfully sent to %s', $final_to), LOG_INFO, __FILE__, __LINE__);
+            return true;
+        } else {
+            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__);
+            return false;
+        }
+    }
+
+    /**
+     * Validates an email address based on the recommendations in RFC 3696.
+     * Is more loose than restrictive, to allow the many valid variants of
+     * email addresses while catching the most common mistakes. Checks an array too.
+     * http://www.faqs.org/rfcs/rfc822.html
+     * http://www.faqs.org/rfcs/rfc2822.html
+     * http://www.faqs.org/rfcs/rfc3696.html
+     * http://www.faqs.org/rfcs/rfc1035.html
+     *
+     * @access  public
+     * @param   mixed  $email  Address to check, string or array.
+     * @return  bool    Validity of address.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   30 Nov 2005 22:00:50
+     */
+    function validEmail($email)
+    {
+        // If an array, check values recursively.
+        if (is_array($email)) {
+            foreach ($email as $e) {
+                if (!$this->validEmail($e)) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            // To be valid email address must match regex and fit within the lenth constraints.
+            if (preg_match($this->getParam('regex'), $email, $e_parts) && mb_strlen($e_parts[2]) < 64 && mb_strlen($e_parts[3]) < 255) {
+                return true;
+            } else {
+                logMsg(sprintf('Invalid email: %s', $email), LOG_INFO, __FILE__, __LINE__);
+                return false;
+            }
+        }
+    }
+}
+
+?>
