* Copyright 2001-2012 Strangecode, LLC * * This file is part of The Strangecode Codebase. * * The Strangecode Codebase is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your option) * any later version. * * The Strangecode Codebase is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * The Strangecode Codebase. If not, see . */ /* * Email.inc.php * * Easy email template usage. * * @author Quinn Comendant * @version 1.0 * * Example of use: --------------------------------------------------------------------- // Setup email object. $email = new Email(array( 'to' => array($frm['email'], 'q@lovemachine.local'), 'from' => sprintf('"%s" <%s>', addcslashes($app->getParam('site_name'), '"'), $app->getParam('site_email')), 'subject' => 'Your account has been activated', )); $email->setTemplate('email_registration_confirm.eml'); // $email->setString('Or you can pass your message body as a string, also with {VARIABLES}.'); $email->replace(array( 'site_name' => $app->getParam('site_name'), 'site_url' => $app->getParam('site_url'), 'username' => $frm['username'], 'password' => $frm['password1'], )); if ($email->send()) { $app->raiseMsg(sprintf(_("A confirmation email has been sent to %s."), $frm['email']), MSG_SUCCESS, __FILE__, __LINE__); } else { $app->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() protected $_params = array( 'to' => null, 'from' => null, 'subject' => null, 'headers' => null, 'envelope_sender_address' => null, // AKA the bounce-to address. Will default to 'from' if left 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, 'sandbox_mode' => null, 'sandbox_to_addr' => null, ); // String that contains filename of the template used (for logging). protected $_template_filename; // String that contains the email body. protected $_template; // String that contains the email body after replacements. protected $_template_replaced; // Email debug modes. const SANDBOX_MODE_REDIRECT = 1; // Send all mail to 'sandbox_to_addr' const SANDBOX_MODE_STDERR = 2; // Log all mail to stderr const SANDBOX_MODE_LOG = 3; // Log all mail using $app->logMsg(…) /** * Constructor. * * @access public * @param array $params Array of object parameters. * @author Quinn Comendant * @since 28 Nov 2005 12:59:41 */ public function __construct($params=null) { $app =& App::getInstance(); // The regex used in validEmail(). Set here instead of in the default _params above so we can use the concatenation . dot. // This matches a (valid) email address as complex as: // "Jane & Bob Smith" (Sales department) // ...and something as simple as: // x@x.com $this->setParam(array('regex' => '/^(?:(?:"[^"]*?"\s*|[^,@]*)(<\s*)|(?:"[^"]*?"|[^,@]*)\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,19}))' // FALSE, matches domain name . '(?(1)' // Comment conditional for if initial < exists . '(?:\s*>\s*|>\s+\([^,@]+\)\s*)' // TRUE, ensure ending > . '|' . '(?:|\s*|\s+\([^,@]+\)\s*))$/i' . $app->getParam('preg_u'))); // 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). */ public function setParam($params) { $app =& App::getInstance(); 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; } if (isset($params['envelope_sender_address']) && !$this->validEmail($params['envelope_sender_address'])) { $params['envelope_sender_address'] = null; } // Merge new parameters with old overriding only those passed. $this->_params = array_merge($this->_params, $params); } else { $app->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. */ public function getParam($param) { $app =& App::getInstance(); if (array_key_exists($param, $this->_params)) { return $this->_params[$param]; } else { $app->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 * @since 28 Nov 2005 12:56:23 */ public function setTemplate($template) { $app =& App::getInstance(); // Load file, using include_path. if (!$this->_template = file_get_contents($template, true)) { $app->logMsg(sprintf('Email template file does not exist: %s', $template), LOG_ERR, __FILE__, __LINE__); $this->_template = null; $this->_template_replaced = null; return false; } // Ensure template is UTF-8. $detected_encoding = mb_detect_encoding($this->_template, array('UTF-8', 'ISO-8859-1', 'WINDOWS-1252'), true); if ('UTF-8' != strtoupper($detected_encoding)) { $this->_template = mb_convert_encoding($this->_template, 'UTF-8', $detected_encoding); } // This could be a new template, so reset the _template_replaced. $this->_template_replaced = null; $this->_template_filename = $template; return true; } /** * Loads template from string to generate email body. * * @access public * @param string $template Filename of email template. * @author Quinn Comendant * @since 28 Nov 2005 12:56:23 */ public function setString($string) { $app =& App::getInstance(); if ('' == trim($string)) { $app->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; $this->_template_filename = '(using Email::setString)'; 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 * @since 28 Nov 2005 13:08:51 */ public function replace($replacements) { $app =& App::getInstance(); // Ensure template exists. if (!isset($this->_template)) { $app->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)) { $app->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, function (&$v) { $v = sprintf('{%s}', 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); } /* * Returns the body of the current email. This can be used to store the message that is being sent. * It will use the original template, or the replaced template if it has been processed. * You can also use this function to do post-processing on the email body before sending it, * like removing extraneous lines: * $email->setString(preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/su', "\n\n", $email->getBody())); * * @access public * @return string Message body. * @author Quinn Comendant * @version 1.0 * @since 18 Nov 2014 21:15:19 */ public function getBody() { $app =& App::getInstance(); $final_body = isset($this->_template_replaced) ? $this->_template_replaced : $this->_template; // Ensure all placeholders have been replaced. Find anything with {...} characters. if (preg_match('/({[^}]+})/', $final_body, $unreplaced_match)) { unset($unreplaced_match[0]); $app->logMsg(sprintf('Cannot get email body. Unreplaced variable(s) "%s" in template "%s"', getDump($unreplaced_match), $this->_template_filename), LOG_ERR, __FILE__, __LINE__); return false; } return $final_body; } /** * Send email using PHP's mail() function. * * @access public * @param string $to * @param string $from * @param string $subject * @author Quinn Comendant * @since 28 Nov 2005 12:56:09 */ public function send($to=null, $from=null, $subject=null, $headers=null) { $app =& App::getInstance(); // 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'])) { $app->logMsg('Cannot send email. SUBJECT not defined.', LOG_ERR, __FILE__, __LINE__); return false; } else if (!isset($this->_template)) { $app->logMsg(sprintf('Cannot send email: "%s". Template not set.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__); return false; } else if (!isset($this->_params['to'])) { $app->logMsg(sprintf('Cannot send email: "%s". TO not defined.', $this->_params['subject']), LOG_NOTICE, __FILE__, __LINE__); return false; } else if (!isset($this->_params['from'])) { $app->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)) { unset($unreplaced_match[0]); $app->logMsg(sprintf('Unreplaced variable(s) "%s" in template "%s"', getDump($unreplaced_match), $this->_template_filename), 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_arr = array(); $final_headers = ''; foreach ($headers as $key => $val) { // Validate key and values. if (!strlen($val)) { $app->logMsg(sprintf('Empty email header provided: %s', $key), LOG_NOTICE, __FILE__, __LINE__); continue; } if (!strlen($key) || preg_match("/[\n\r]/", $key . $val) || preg_match('/[^\w-]/', $key)) { $app->logMsg(sprintf('Broken email header provided: %s=%s', $key, $val), LOG_WARNING, __FILE__, __LINE__); continue; } // If the envelope_sender_address was given as a header, move it to the correct place. if ('envelope_sender_address' == strtolower($key)) { $this->_params['envelope_sender_address'] = isset($this->_params['envelope_sender_address']) ? $this->_params['envelope_sender_address'] : $val; continue; } // If we're sending in sandbox mode, remove any headers with recipient addresses. if ($this->getParam('sandbox_mode') == self::SANDBOX_MODE_REDIRECT && in_array(strtolower($key), array('to', 'cc', 'bcc')) && mb_strpos($val, '@') !== false) { // Don't carry this into the $final_headers. $app->logMsg(sprintf('Skipping header in sandbox mode: %s=%s', $key, $val), LOG_DEBUG, __FILE__, __LINE__); continue; } $final_headers_arr[] = sprintf('%s: %s', $key, $val); } $final_headers = join($this->getParam('crlf'), $final_headers_arr); // This is the address where delivery problems are sent to. We must strip off everything except the local@domain part. if (isset($this->_params['envelope_sender_address'])) { $envelope_sender_address = sprintf('<%s>', trim($this->_params['envelope_sender_address'], '<>')); } else { $envelope_sender_address = preg_replace('/^.*()]+\@[A-Za-z0-9.-]{1,}\.[A-Za-z]{2,19})>?$/iU' . $app->getParam('preg_u'), '$1', $this->_params['from']); } if ('' != $envelope_sender_address && $this->validEmail($envelope_sender_address)) { $additional_parameter = sprintf('-f %s', $envelope_sender_address); } else { $additional_parameter = ''; } // 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)\s*:/i", $full_mail_content)) { $app->logMsg(sprintf('Mail header injection attack in content: %s', $full_mail_content), LOG_WARNING, __FILE__, __LINE__); return false; } // Enter sandbox mode, if specified. switch ($this->getParam('sandbox_mode')) { case self::SANDBOX_MODE_REDIRECT: if (!$this->getParam('sandbox_to_addr')) { $app->logMsg(sprintf('Email sandbox_mode is SANDBOX_MODE_REDIRECT but sandbox_to_addr is not set.', null), LOG_ERR, __FILE__, __LINE__); break; } $final_to = $this->getParam('sandbox_to_addr'); break; case self::SANDBOX_MODE_STDERR: file_put_contents('php://stderr', sprintf("Subject: %s\nTo: %s\n%s\n\n%s", $this->getParam('subject'), $final_to, str_replace($this->getParam('crlf'), "\n", $final_headers), $final_body), FILE_APPEND); return true; case self::SANDBOX_MODE_LOG: // Temporarily modify log settings to allow full multi-line emails to appear in logs. $log_serialize = $app->getParam('log_serialize'); $log_message_max_length = $app->getParam('log_message_max_length'); $app->setParam(array('log_serialize' => false, 'log_message_max_length' => 65536)); $app->logMsg(sprintf("\nSubject: %s\nTo: %s\n%s\n\n%s", $this->getParam('subject'), $final_to, trim(str_replace($this->getParam('crlf'), "\n", $final_headers)), trim($final_body)), LOG_DEBUG, __FILE__, __LINE__); $app->setParam(array('log_serialize' => $log_serialize, 'log_message_max_length' => $log_message_max_length)); return true; } // Send email without 5th parameter if safemode is enabled. if (ini_get('safe_mode')) { $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers); } else { $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers, $additional_parameter); } // Ensure message was successfully accepted for delivery. if ($ret) { $app->logMsg(sprintf('Email successfully sent to %s', $final_to), LOG_INFO, __FILE__, __LINE__); return true; } else { $app->logMsg(sprintf('Email failure: %s, %s, %s, %s', $final_to, $this->_params['subject'], str_replace("\r\n", '\r\n', $final_headers), $additional_parameter), LOG_WARNING, __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 * @since 30 Nov 2005 22:00:50 */ public function validEmail($email) { $app =& App::getInstance(); // 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 length 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 { $app->logMsg(sprintf('Invalid email address: %s', $email), LOG_INFO, __FILE__, __LINE__); return false; } } } }