/classes/fEmail.php
PHP | 1805 lines | 1283 code | 150 blank | 372 comment | 78 complexity | 138b53adeb3f4e5cdc1e298d02015b44 MD5 | raw file
- <?php
- /**
- * Allows creating and sending a single email containing plaintext, HTML, attachments and S/MIME encryption
- *
- * Please note that this class uses the [http://php.net/function.mail mail()]
- * function by default. Developers that are sending multiple emails, or need
- * SMTP support, should use fSMTP with this class.
- *
- * This class is implemented to use the UTF-8 character encoding. Please see
- * http://flourishlib.com/docs/UTF-8 for more information.
- *
- * @copyright Copyright (c) 2008-2011 Will Bond, others
- * @author Will Bond [wb] <will@flourishlib.com>
- * @author Bill Bushee, iMarc LLC [bb-imarc] <bill@imarc.net>
- * @author netcarver [n] <fContrib@netcarving.com>
- * @license http://flourishlib.com/license
- *
- * @package Flourish
- * @link http://flourishlib.com/fEmail
- *
- * @version 1.0.0b30
- * @changes 1.0.0b30 Changed methods to return instance for method chaining [n, 2011-09-12]
- * @changes 1.0.0b29 Changed ::combineNameEmail() to be a static method and to be exposed publicly for use by other classes [wb, 2011-07-26]
- * @changes 1.0.0b28 Fixed ::addAttachment() and ::addRelatedFile() to properly handle duplicate filenames [wb, 2011-05-17]
- * @changes 1.0.0b27 Fixed a bug with generating FQDNs on some Windows machines [wb, 2011-02-24]
- * @changes 1.0.0b26 Added ::addCustomerHeader() [wb, 2011-02-02]
- * @changes 1.0.0b25 Fixed a bug with finding the FQDN on non-Windows machines [wb, 2011-01-19]
- * @changes 1.0.0b24 Backwards Compatibility Break - the `$contents` parameter of ::addAttachment() is now first instead of third, ::addAttachment() will now accept fFile objects for the `$contents` parameter, added ::addRelatedFile() [wb, 2010-12-01]
- * @changes 1.0.0b23 Fixed a bug on Windows where emails starting with a `.` would have the `.` removed [wb, 2010-09-11]
- * @changes 1.0.0b22 Revamped the FQDN code and added ::getFQDN() [wb, 2010-09-07]
- * @changes 1.0.0b21 Added a check to prevent permissions warnings when getting the FQDN on Windows machines [wb, 2010-09-02]
- * @changes 1.0.0b20 Fixed ::send() to only remove the name of a recipient when dealing with the `mail()` function on Windows and to leave it when using fSMTP [wb, 2010-06-22]
- * @changes 1.0.0b19 Changed ::send() to return the message id for the email, fixed the email regexes to require [] around IPs [wb, 2010-05-05]
- * @changes 1.0.0b18 Fixed the name of the static method ::unindentExpand() [wb, 2010-04-28]
- * @changes 1.0.0b17 Added the static method ::unindentExpand() [wb, 2010-04-26]
- * @changes 1.0.0b16 Added support for sending emails via fSMTP [wb, 2010-04-20]
- * @changes 1.0.0b15 Added the `$unindent_expand_constants` parameter to ::setBody(), added ::loadBody() and ::loadHTMLBody(), fixed HTML emails with attachments [wb, 2010-03-14]
- * @changes 1.0.0b14 Changed ::send() to not double `.`s at the beginning of lines on Windows since it seemed to break things rather than fix them [wb, 2010-03-05]
- * @changes 1.0.0b13 Fixed the class to work when safe mode is turned on [wb, 2009-10-23]
- * @changes 1.0.0b12 Removed duplicate MIME-Version headers that were being included in S/MIME encrypted emails [wb, 2009-10-05]
- * @changes 1.0.0b11 Updated to use the new fValidationException API [wb, 2009-09-17]
- * @changes 1.0.0b10 Fixed a bug with sending both an HTML and a plaintext body [bb-imarc, 2009-06-18]
- * @changes 1.0.0b9 Fixed a bug where the MIME headers were not being set for all emails [wb, 2009-06-12]
- * @changes 1.0.0b8 Added the method ::clearRecipients() [wb, 2009-05-29]
- * @changes 1.0.0b7 Email names with UTF-8 characters are now properly encoded [wb, 2009-05-08]
- * @changes 1.0.0b6 Fixed a bug where <> quoted email addresses in validation messages were not showing [wb, 2009-03-27]
- * @changes 1.0.0b5 Updated for new fCore API [wb, 2009-02-16]
- * @changes 1.0.0b4 The recipient error message in ::validate() no longer contains a typo [wb, 2009-02-09]
- * @changes 1.0.0b3 Fixed a bug with missing content in the fValidationException thrown by ::validate() [wb, 2009-01-14]
- * @changes 1.0.0b2 Fixed a few bugs with sending S/MIME encrypted/signed emails [wb, 2009-01-10]
- * @changes 1.0.0b The initial implementation [wb, 2008-06-23]
- */
- class fEmail
- {
- // The following constants allow for nice looking callbacks to static methods
- const combineNameEmail = 'fEmail::combineNameEmail';
- const fixQmail = 'fEmail::fixQmail';
- const getFQDN = 'fEmail::getFQDN';
- const reset = 'fEmail::reset';
- const unindentExpand = 'fEmail::unindentExpand';
-
-
- /**
- * A regular expression to match an email address, exluding those with comments and folding whitespace
- *
- * The matches will be:
- *
- * - `[0]`: The whole email address
- * - `[1]`: The name before the `@`
- * - `[2]`: The domain/ip after the `@`
- *
- * @var string
- */
- const EMAIL_REGEX = '~^[ \t]*( # Allow leading whitespace
- (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+") # An "atom" or a quoted string
- (?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))* # A . plus another "atom" or a quoted string, any number of times
- )@( # The @ symbol
- (?:[a-z0-9\\-]+\.)+[a-z]{2,}| # Domain name
- \[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\] # (or) IP addresses
- )[ \t]*$~ixD'; # Allow Trailing whitespace
-
- /**
- * A regular expression to match a `name <email>` string, exluding those with comments and folding whitespace
- *
- * The matches will be:
- *
- * - `[0]`: The whole name and email address
- * - `[1]`: The name
- * - `[2]`: The whole email address
- * - `[3]`: The email username before the `@`
- * - `[4]`: The email domain/ip after the `@`
- *
- * @var string
- */
- const NAME_EMAIL_REGEX = '~^[ \t]*( # Allow leading whitespace
- (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*) # An "atom" or a quoted string
- (?:\.?[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*))*) # Another "atom" or a quoted string or a . followed by one of those, any number of times
- [ \t]*<[ \t]*(( # The < encapsulating the email address
- (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+") # An "atom" or a quoted string
- (?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))* # A . plus another "atom" or a quoted string, any number of times
- )@( # The @ symbol
- (?:[a-z0-9\\-]+\.)+[a-z]{2,}| # Domain nam
- \[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\] # (or) IP addresses
- ))[ \t]*>[ \t]*$~ixD'; # Closing > and trailing whitespace
-
-
- /**
- * Flags if the class should convert `\r\n` to `\n` for qmail. This makes invalid email headers that may work.
- *
- * @var boolean
- */
- static private $convert_crlf = FALSE;
-
- /**
- * The local fully-qualified domain name
- */
- static private $fqdn;
-
- /**
- * Flags if the class should use [http://php.net/popen popen()] to send mail via sendmail
- *
- * @var boolean
- */
- static private $popen_sendmail = FALSE;
- /**
- * Turns a name and email into a `"name" <email>` string, or just `email` if no name is provided
- *
- * This method will remove newline characters from the name and email, and
- * will remove any backslash (`\`) and double quote (`"`) characters from
- * the name.
- *
- * @internal
- *
- * @param string $name The name associated with the email address
- * @param string $email The email address
- * @return string The '"name" <email>' or 'email' string
- */
- static public function combineNameEmail($name, $email)
- {
- // Strip lower ascii character since they aren't useful in email addresses
- $email = preg_replace('#[\x0-\x19]+#', '', $email);
- $name = preg_replace('#[\x0-\x19]+#', '', $name);
-
- if (!$name) {
- return $email;
- }
-
- // If the name contains any non-ascii bytes or stuff not allowed
- // in quoted strings we just make an encoded word out of it
- if (preg_replace('#[\x80-\xff\x5C\x22]#', '', $name) != $name) {
- // The longest header name that will contain email addresses is
- // "Bcc: ", which is 5 characters long
- $name = self::makeEncodedWord($name, 5);
- } else {
- $name = '"' . $name . '"';
- }
-
- return $name . ' <' . $email . '>';
- }
-
-
- /**
- * Composes text using fText if loaded
- *
- * @param string $message The message to compose
- * @param mixed $component A string or number to insert into the message
- * @param mixed ...
- * @return string The composed and possible translated message
- */
- static protected function compose($message)
- {
- $args = array_slice(func_get_args(), 1);
-
- if (class_exists('fText', FALSE)) {
- return call_user_func_array(
- array('fText', 'compose'),
- array($message, $args)
- );
- } else {
- return vsprintf($message, $args);
- }
- }
-
-
- /**
- * Sets the class to try and fix broken qmail implementations that add `\r` to `\r\n`
- *
- * Before trying to fix qmail with this method, please try using fSMTP
- * to connect to `localhost` and pass the fSMTP object to ::send().
- *
- * @return void
- */
- static public function fixQmail()
- {
- if (fCore::checkOS('windows')) {
- return;
- }
-
- $sendmail_command = ini_get('sendmail_path');
-
- if (!$sendmail_command) {
- self::$convert_crlf = TRUE;
- trigger_error(
- self::compose('The proper fix for sending through qmail is not possible since the sendmail path is not set'),
- E_USER_WARNING
- );
- trigger_error(
- self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
- E_USER_WARNING
- );
- }
-
- $sendmail_command_parts = explode(' ', $sendmail_command, 2);
-
- $sendmail_path = $sendmail_command_parts[0];
- $sendmail_dir = pathinfo($sendmail_path, PATHINFO_DIRNAME);
- $sendmail_params = (isset($sendmail_command_parts[1])) ? $sendmail_command_parts[1] : '';
-
- // Check to see if we can run sendmail via popen
- $executable = FALSE;
- $safe_mode = FALSE;
-
- if (!in_array(strtolower(ini_get('safe_mode')), array('0', '', 'off'))) {
- $safe_mode = TRUE;
- $exec_dirs = explode(';', ini_get('safe_mode_exec_dir'));
- foreach ($exec_dirs as $exec_dir) {
- if (stripos($sendmail_dir, $exec_dir) !== 0) {
- continue;
- }
- if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
- $executable = TRUE;
- }
- }
-
- } else {
- if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
- $executable = TRUE;
- }
- }
-
- if ($executable) {
- self::$popen_sendmail = TRUE;
- } else {
- self::$convert_crlf = TRUE;
- if ($safe_mode) {
- trigger_error(
- self::compose('The proper fix for sending through qmail is not possible since safe mode is turned on and the sendmail binary is not in one of the paths defined by the safe_mode_exec_dir ini setting'),
- E_USER_WARNING
- );
- trigger_error(
- self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
- E_USER_WARNING
- );
- } else {
- trigger_error(
- self::compose('The proper fix for sending through qmail is not possible since the sendmail binary could not be found or is not executable'),
- E_USER_WARNING
- );
- trigger_error(
- self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
- E_USER_WARNING
- );
- }
- }
- }
-
-
- /**
- * Returns the fully-qualified domain name of the server
- *
- * @internal
- *
- * @return string The fully-qualified domain name of the server
- */
- static public function getFQDN()
- {
- if (self::$fqdn !== NULL) {
- return self::$fqdn;
- }
-
- if (isset($_ENV['HOST'])) {
- self::$fqdn = $_ENV['HOST'];
- }
- if (strpos(self::$fqdn, '.') === FALSE && isset($_ENV['HOSTNAME'])) {
- self::$fqdn = $_ENV['HOSTNAME'];
- }
- if (strpos(self::$fqdn, '.') === FALSE) {
- self::$fqdn = php_uname('n');
- }
-
- if (strpos(self::$fqdn, '.') === FALSE) {
-
- $can_exec = !in_array('exec', array_map('trim', explode(',', ini_get('disable_functions')))) && !ini_get('safe_mode');
- if (fCore::checkOS('linux') && $can_exec) {
- self::$fqdn = trim(shell_exec('hostname --fqdn'));
-
- } elseif (fCore::checkOS('windows')) {
- $shell = new COM('WScript.Shell');
- $tcpip_key = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip';
- try {
- $domain = $shell->RegRead($tcpip_key . '\Parameters\NV Domain');
- } catch (com_exception $e) {
- try {
- $domain = $shell->RegRead($tcpip_key . '\Parameters\DhcpDomain');
- } catch (com_exception $e) {
- try {
- $adapters = $shell->RegRead($tcpip_key . '\Linkage\Route');
- foreach ($adapters as $adapter) {
- if ($adapter[0] != '{') { continue; }
- try {
- $domain = $shell->RegRead($tcpip_key . '\Interfaces\\' . $adapter . '\Domain');
- } catch (com_exception $e) {
- try {
- $domain = $shell->RegRead($tcpip_key . '\Interfaces\\' . $adapter . '\DhcpDomain');
- } catch (com_exception $e) { }
- }
- }
- } catch (com_exception $e) { }
- }
- }
- if (!empty($domain)) {
- self::$fqdn .= '.' . $domain;
- }
-
- } elseif (!fCore::checkOS('windows') && !ini_get('open_basedir') && file_exists('/etc/resolv.conf')) {
- $output = file_get_contents('/etc/resolv.conf');
- if (preg_match('#^domain ([a-z0-9_.-]+)#im', $output, $match)) {
- self::$fqdn .= '.' . $match[1];
- }
- }
- }
-
- return self::$fqdn;
- }
- /**
- * Encodes a string to UTF-8 encoded-word
- *
- * @param string $content The content to encode
- * @param integer $first_line_prefix_length The length of any prefix applied to the first line of the encoded word - this allows properly accounting for a header name
- * @return string The encoded string
- */
- static private function makeEncodedWord($content, $first_line_prefix_length)
- {
- // Homogenize the line-endings to CRLF
- $content = str_replace("\r\n", "\n", $content);
- $content = str_replace("\r", "\n", $content);
- $content = str_replace("\n", "\r\n", $content);
-
- // Encoded word is not required if all characters are ascii
- if (!preg_match('#[\x80-\xFF]#', $content)) {
- return $content;
- }
-
- // A quick a dirty hex encoding
- $content = rawurlencode($content);
- $content = str_replace('=', '%3D', $content);
- $content = str_replace('%', '=', $content);
-
- // Decode characters that don't have to be coded
- $decodings = array(
- '=20' => '_', '=21' => '!', '=22' => '"', '=23' => '#',
- '=24' => '$', '=25' => '%', '=26' => '&', '=27' => "'",
- '=28' => '(', '=29' => ')', '=2A' => '*', '=2B' => '+',
- '=2C' => ',', '=2D' => '-', '=2E' => '.', '=2F' => '/',
- '=3A' => ':', '=3B' => ';', '=3C' => '<', '=3E' => '>',
- '=40' => '@', '=5B' => '[', '=5C' => '\\', '=5D' => ']',
- '=5E' => '^', '=60' => '`', '=7B' => '{', '=7C' => '|',
- '=7D' => '}', '=7E' => '~', ' ' => '_'
- );
-
- $content = strtr($content, $decodings);
-
- $length = strlen($content);
-
- $prefix = '=?utf-8?Q?';
- $suffix = '?=';
-
- $prefix_length = 10;
- $suffix_length = 2;
-
- // This loop goes through and ensures we are wrapping by 75 chars
- // including the encoded word delimiters
- $output = $prefix;
- $line_length = $prefix_length + $first_line_prefix_length;
-
- for ($i=0; $i<$length; $i++) {
-
- // Get info about the next character
- $char_length = ($content[$i] == '=') ? 3 : 1;
- $char = $content[$i];
- if ($char_length == 3) {
- $char .= $content[$i+1] . $content[$i+2];
- }
-
- // If we have too long a line, wrap it
- if ($line_length + $suffix_length + $char_length > 75) {
- $output .= $suffix . "\r\n " . $prefix;
- $line_length = $prefix_length + 2;
- }
-
- // Add the character
- $output .= $char;
-
- // Figure out how much longer the line is
- $line_length += $char_length;
-
- // Skip characters if we have an encoded character
- $i += $char_length-1;
- }
-
- if (substr($output, -2) != $suffix) {
- $output .= $suffix;
- }
-
- return $output;
- }
-
-
- /**
- * Resets the configuration of the class
- *
- * @internal
- *
- * @return void
- */
- static public function reset()
- {
- self::$convert_crlf = FALSE;
- self::$fqdn = NULL;
- self::$popen_sendmail = FALSE;
- }
-
-
- /**
- * Returns `TRUE` for non-empty strings, numbers, objects, empty numbers and string-like numbers (such as `0`, `0.0`, `'0'`)
- *
- * @param mixed $value The value to check
- * @return boolean If the value is string-like
- */
- static protected function stringlike($value)
- {
- if ((!is_string($value) && !is_object($value) && !is_numeric($value)) || !strlen(trim($value))) {
- return FALSE;
- }
-
- return TRUE;
- }
-
-
- /**
- * Takes a block of text, unindents it and replaces {CONSTANT} tokens with the constant's value
- *
- * @param string $text The text to unindent and replace constants in
- * @return string The unindented text
- */
- static public function unindentExpand($text)
- {
- $text = preg_replace('#^[ \t]*\n|\n[ \t]*$#D', '', $text);
-
- if (preg_match('#^[ \t]+(?=\S)#m', $text, $match)) {
- $text = preg_replace('#^' . preg_quote($match[0]) . '#m', '', $text);
- }
-
- preg_match_all('#\{([a-z][a-z0-9_]*)\}#i', $text, $constants, PREG_SET_ORDER);
- foreach ($constants as $constant) {
- if (!defined($constant[1])) { continue; }
- $text = preg_replace('#' . preg_quote($constant[0], '#') . '#', constant($constant[1]), $text, 1);
- }
-
- return $text;
- }
-
-
- /**
- * The file contents to attach
- *
- * @var array
- */
- private $attachments = array();
-
- /**
- * The email address(es) to BCC to
- *
- * @var array
- */
- private $bcc_emails = array();
-
- /**
- * The email address to bounce to
- *
- * @var string
- */
- private $bounce_to_email = NULL;
-
- /**
- * The email address(es) to CC to
- *
- * @var array
- */
- private $cc_emails = array();
-
- /**
- * Custom headers
- *
- * @var array
- */
- private $custom_headers = array();
-
- /**
- * The email address being sent from
- *
- * @var string
- */
- private $from_email = NULL;
-
- /**
- * The HTML body of the email
- *
- * @var string
- */
- private $html_body = NULL;
-
- /**
- * The Message-ID header for the email
- *
- * @var string
- */
- private $message_id = NULL;
-
- /**
- * The plaintext body of the email
- *
- * @var string
- */
- private $plaintext_body = NULL;
-
- /**
- * The recipient's S/MIME PEM certificate filename, used for encryption of the message
- *
- * @var string
- */
- private $recipients_smime_cert_file = NULL;
-
- /**
- * The files to include as multipart/related
- *
- * @var array
- */
- private $related_files = array();
-
- /**
- * The email address to reply to
- *
- * @var string
- */
- private $reply_to_email = NULL;
-
- /**
- * The email address actually sending the email
- *
- * @var string
- */
- private $sender_email = NULL;
-
- /**
- * The senders's S/MIME PEM certificate filename, used for singing the message
- *
- * @var string
- */
- private $senders_smime_cert_file = NULL;
-
- /**
- * The senders's S/MIME private key filename, used for singing the message
- *
- * @var string
- */
- private $senders_smime_pk_file = NULL;
-
- /**
- * The senders's S/MIME private key password, used for singing the message
- *
- * @var string
- */
- private $senders_smime_pk_password = NULL;
-
- /**
- * If the message should be encrypted using the recipient's S/MIME certificate
- *
- * @var boolean
- */
- private $smime_encrypt = FALSE;
-
- /**
- * If the message should be signed using the senders's S/MIME private key
- *
- * @var boolean
- */
- private $smime_sign = FALSE;
-
- /**
- * The subject of the email
- *
- * @var string
- */
- private $subject = NULL;
-
- /**
- * The email address(es) to send to
- *
- * @var array
- */
- private $to_emails = array();
-
-
- /**
- * Initializes fEmail for creating message ids
- *
- * @return fEmail
- */
- public function __construct()
- {
- $this->message_id = '<' . fCryptography::randomString(10, 'hexadecimal') . '.' . time() . '@' . self::getFQDN() . '>';
- }
-
-
- /**
- * All requests that hit this method should be requests for callbacks
- *
- * @internal
- *
- * @param string $method The method to create a callback for
- * @return callback The callback for the method requested
- */
- public function __get($method)
- {
- return array($this, $method);
- }
-
-
- /**
- * Adds an attachment to the email
- *
- * If a duplicate filename is detected, it will be changed to be unique.
- *
- * @param string|fFile $contents The contents of the file
- * @param string $filename The name to give the attachement - optional if `$contents` is an fFile object
- * @param string $mime_type The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
- * @return fEmail The email object, to allow for method chaining
- */
- public function addAttachment($contents, $filename=NULL, $mime_type=NULL)
- {
- $this->extrapolateFileInfo($contents, $filename, $mime_type);
-
- while (isset($this->attachments[$filename])) {
- $filename = $this->generateNewFilename($filename);
- }
-
- $this->attachments[$filename] = array(
- 'mime-type' => $mime_type,
- 'contents' => $contents
- );
- return $this;
- }
-
-
- /**
- * Adds a “related� file to the email, returning the `Content-ID` for use in HTML
- *
- * The purpose of a related file is to be able to reference it in part of
- * the HTML body. Image `src` URLs can reference a related file by starting
- * the URL with `cid:` and then inserting the `Content-ID`.
- *
- * If a duplicate filename is detected, it will be changed to be unique.
- *
- * @param string|fFile $contents The contents of the file
- * @param string $filename The name to give the attachement - optional if `$contents` is an fFile object
- * @param string $mime_type The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
- * @return string The fully-formed `cid:` URL for use in HTML `src` attributes
- */
- public function addRelatedFile($contents, $filename=NULL, $mime_type=NULL)
- {
- $this->extrapolateFileInfo($contents, $filename, $mime_type);
-
- while (isset($this->related_files[$filename])) {
- $filename = $this->generateNewFilename($filename);
- }
-
- $cid = count($this->related_files) . '.' . substr($this->message_id, 1, -1);
-
- $this->related_files[$filename] = array(
- 'mime-type' => $mime_type,
- 'contents' => $contents,
- 'content-id' => '<' . $cid . '>'
- );
-
- return 'cid:' . $cid;
- }
-
-
- /**
- * Adds a blind carbon copy (BCC) email recipient
- *
- * @param string $email The email address to BCC
- * @param string $name The recipient's name
- * @return fEmail The email object, to allow for method chaining
- */
- public function addBCCRecipient($email, $name=NULL)
- {
- if (!$email) {
- return;
- }
-
- $this->bcc_emails[] = self::combineNameEmail($name, $email);
- return $this;
- }
-
-
- /**
- * Adds a carbon copy (CC) email recipient
- *
- * @param string $email The email address to BCC
- * @param string $name The recipient's name
- * @return fEmail The email object, to allow for method chaining
- */
- public function addCCRecipient($email, $name=NULL)
- {
- if (!$email) {
- return;
- }
-
- $this->cc_emails[] = self::combineNameEmail($name, $email);
- return $this;
- }
-
-
- /**
- * Allows adding a custom header to the email
- *
- * If the method is called multiple times with the same name, the last
- * value will be used.
- *
- * Please note that this class will properly format the header, including
- * adding the `:` between the name and value and wrapping values that are
- * too long for a single line.
- *
- * @param string $name The name of the header
- * @param string $value The value of the header
- * @param array :$headers An associative array of `{name} => {value}`
- * @return fEmail The email object, to allow for method chaining
- */
- public function addCustomHeader($name, $value=NULL)
- {
- if ($value === NULL && is_array($name)) {
- foreach ($name as $key => $value) {
- $this->addCustomHeader($key, $value);
- }
- return;
- }
-
- $lower_name = fUTF8::lower($name);
- $this->custom_headers[$lower_name] = array($name, $value);
- return $this;
- }
-
-
- /**
- * Adds an email recipient
- *
- * @param string $email The email address to send to
- * @param string $name The recipient's name
- * @return fEmail The email object, to allow for method chaining
- */
- public function addRecipient($email, $name=NULL)
- {
- if (!$email) {
- return;
- }
-
- $this->to_emails[] = self::combineNameEmail($name, $email);
- return $this;
- }
-
-
- /**
- * Takes a multi-address email header and builds it out using an array of emails
- *
- * @param string $header The header name without `': '`, the header is non-blank, `': '` will be added
- * @param array $emails The email addresses for the header
- * @return string The email header with a trailing `\r\n`
- */
- private function buildMultiAddressHeader($header, $emails)
- {
- $header .= ': ';
-
- $first = TRUE;
- $line = 1;
- foreach ($emails as $email) {
- if ($first) { $first = FALSE; } else { $header .= ', '; }
-
- // Try to stay within the recommended 78 character line limit
- $last_crlf_pos = (integer) strrpos($header, "\r\n");
- if (strlen($header . $email) - $last_crlf_pos > 78) {
- $header .= "\r\n ";
- $line++;
- }
-
- $header .= trim($email);
- }
-
- return $header . "\r\n";
- }
-
-
- /**
- * Removes all To, CC and BCC recipients from the email
- *
- * @return fEmail The email object, to allow for method chaining
- */
- public function clearRecipients()
- {
- $this->to_emails = array();
- $this->cc_emails = array();
- $this->bcc_emails = array();
- return $this;
- }
-
-
- /**
- * Creates a 32-character boundary for a multipart message
- *
- * @return string A multipart boundary
- */
- private function createBoundary()
- {
- $chars = 'ancdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:-_';
- $last_index = strlen($chars) - 1;
- $output = '';
-
- for ($i = 0; $i < 28; $i++) {
- $output .= $chars[rand(0, $last_index)];
- }
- return $output;
- }
-
-
- /**
- * Builds the body of the email
- *
- * @param string $boundary The boundary to use for the top level mime block
- * @return string The message body to be sent to the mail() function
- */
- private function createBody($boundary)
- {
- $boundary_stack = array($boundary);
-
- $mime_notice = self::compose(
- "This message has been formatted using MIME. It does not appear that your\r\nemail client supports MIME."
- );
-
- $body = '';
-
- if ($this->html_body || $this->attachments) {
- $body .= $mime_notice . "\r\n\r\n";
- }
-
- if ($this->html_body && $this->related_files && $this->attachments) {
- $body .= '--' . end($boundary_stack) . "\r\n";
- $boundary_stack[] = $this->createBoundary();
- $body .= 'Content-Type: multipart/related; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
- }
-
- if ($this->html_body && ($this->attachments || $this->related_files)) {
- $body .= '--' . end($boundary_stack) . "\r\n";
- $boundary_stack[] = $this->createBoundary();
- $body .= 'Content-Type: multipart/alternative; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
- }
-
- if ($this->html_body || $this->attachments) {
- $body .= '--' . end($boundary_stack) . "\r\n";
- $body .= "Content-Type: text/plain; charset=utf-8\r\n";
- $body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
- }
-
- $body .= $this->makeQuotedPrintable($this->plaintext_body) . "\r\n";
-
- if ($this->html_body) {
- $body .= '--' . end($boundary_stack) . "\r\n";
- $body .= "Content-Type: text/html; charset=utf-8\r\n";
- $body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
- $body .= $this->makeQuotedPrintable($this->html_body) . "\r\n";
- }
-
- if ($this->related_files) {
- $body .= '--' . end($boundary_stack) . "--\r\n";
- array_pop($boundary_stack);
-
- foreach ($this->related_files as $filename => $file_info) {
- $body .= '--' . end($boundary_stack) . "\r\n";
- $body .= 'Content-Type: ' . $file_info['mime-type'] . '; name="' . $filename . "\"\r\n";
- $body .= "Content-Transfer-Encoding: base64\r\n";
- $body .= 'Content-ID: ' . $file_info['content-id'] . "\r\n\r\n";
- $body .= $this->makeBase64($file_info['contents']) . "\r\n";
- }
- }
-
- if ($this->attachments) {
-
- if ($this->html_body) {
- $body .= '--' . end($boundary_stack) . "--\r\n";
- array_pop($boundary_stack);
- }
-
- foreach ($this->attachments as $filename => $file_info) {
- $body .= '--' . end($boundary_stack) . "\r\n";
- $body .= 'Content-Type: ' . $file_info['mime-type'] . "\r\n";
- $body .= "Content-Transfer-Encoding: base64\r\n";
- $body .= 'Content-Disposition: attachment; filename="' . $filename . "\";\r\n\r\n";
- $body .= $this->makeBase64($file_info['contents']) . "\r\n";
- }
- }
-
- if ($this->html_body || $this->attachments) {
- $body .= '--' . end($boundary_stack) . "--\r\n";
- array_pop($boundary_stack);
- }
-
- return $body;
- }
-
-
- /**
- * Builds the headers for the email
- *
- * @param string $boundary The boundary to use for the top level mime block
- * @param string $message_id The message id for the message
- * @return string The headers to be sent to the [http://php.net/function.mail mail()] function
- */
- private function createHeaders($boundary, $message_id)
- {
- $headers = '';
-
- if ($this->cc_emails) {
- $headers .= $this->buildMultiAddressHeader("Cc", $this->cc_emails);
- }
-
- if ($this->bcc_emails) {
- $headers .= $this->buildMultiAddressHeader("Bcc", $this->bcc_emails);
- }
-
- $headers .= "From: " . trim($this->from_email) . "\r\n";
-
- if ($this->reply_to_email) {
- $headers .= "Reply-To: " . trim($this->reply_to_email) . "\r\n";
- }
-
- if ($this->sender_email) {
- $headers .= "Sender: " . trim($this->sender_email) . "\r\n";
- }
-
- foreach ($this->custom_headers as $header_info) {
- $header_prefix = $header_info[0] . ': ';
- $headers .= $header_prefix . self::makeEncodedWord($header_info[1], strlen($header_prefix)) . "\r\n";
- }
-
- $headers .= "Message-ID: " . $message_id . "\r\n";
- $headers .= "MIME-Version: 1.0\r\n";
-
- if (!$this->html_body && !$this->attachments) {
- $headers .= "Content-Type: text/plain; charset=utf-8\r\n";
- $headers .= "Content-Transfer-Encoding: quoted-printable\r\n";
-
- } elseif ($this->html_body && !$this->attachments) {
- if ($this->related_files) {
- $headers .= 'Content-Type: multipart/related; boundary="' . $boundary . "\"\r\n";
- } else {
- $headers .= 'Content-Type: multipart/alternative; boundary="' . $boundary . "\"\r\n";
- }
-
- } elseif ($this->attachments) {
- $headers .= 'Content-Type: multipart/mixed; boundary="' . $boundary . "\"\r\n";
- }
-
- return $headers . "\r\n";
- }
-
-
- /**
- * Takes the body of the message and processes it with S/MIME
- *
- * @param string $to The recipients being sent to
- * @param string $subject The subject of the email
- * @param string $headers The headers for the message
- * @param string $body The message body
- * @return array `0` => The message headers, `1` => The message body
- */
- private function createSMIMEBody($to, $subject, $headers, $body)
- {
- if (!$this->smime_encrypt && !$this->smime_sign) {
- return array($headers, $body);
- }
-
- $plaintext_file = tempnam('', '__fEmail_');
- $ciphertext_file = tempnam('', '__fEmail_');
-
- $headers_array = array(
- 'To' => $to,
- 'Subject' => $subject
- );
-
- preg_match_all('#^([\w\-]+):\s+([^\n]+\n( [^\n]+\n)*)#im', $headers, $header_matches, PREG_SET_ORDER);
- foreach ($header_matches as $header_match) {
- $headers_array[$header_match[1]] = trim($header_match[2]);
- }
-
- $body_headers = "";
- if (isset($headers_array['Content-Type'])) {
- $body_headers .= 'Content-Type: ' . $headers_array['Content-Type'] . "\r\n";
- }
- if (isset($headers_array['Content-Transfer-Encoding'])) {
- $body_headers .= 'Content-Transfer-Encoding: ' . $headers_array['Content-Transfer-Encoding'] . "\r\n";
- }
-
- if ($body_headers) {
- $body = $body_headers . "\r\n" . $body;
- }
-
- file_put_contents($plaintext_file, $body);
- file_put_contents($ciphertext_file, '');
-
- // Set up the neccessary S/MIME resources
- if ($this->smime_sign) {
- $senders_smime_cert = file_get_contents($this->senders_smime_cert_file);
- $senders_private_key = openssl_pkey_get_private(
- file_get_contents($this->senders_smime_pk_file),
- $this->senders_smime_pk_password
- );
-
- if ($senders_private_key === FALSE) {
- throw new fValidationException(
- "The sender's S/MIME private key password specified does not appear to be valid for the private key"
- );
- }
- }
-
- if ($this->smime_encrypt) {
- $recipients_smime_cert = file_get_contents($this->recipients_smime_cert_file);
- }
-
-
- // If we are going to sign and encrypt, the best way is to sign, encrypt and then sign again
- if ($this->smime_encrypt && $this->smime_sign) {
- openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, array());
- openssl_pkcs7_encrypt($ciphertext_file, $plaintext_file, $recipients_smime_cert, array(), NULL, OPENSSL_CIPHER_RC2_128);
- openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
-
- } elseif ($this->smime_sign) {
- openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
-
- } elseif ($this->smime_encrypt) {
- openssl_pkcs7_encrypt($plaintext_file, $ciphertext_file, $recipients_smime_cert, $headers_array, NULL, OPENSSL_CIPHER_RC2_128);
- }
-
- // It seems that the contents of the ciphertext is not always \r\n line breaks
- $message = file_get_contents($ciphertext_file);
- $message = str_replace("\r\n", "\n", $message);
- $message = str_replace("\r", "\n", $message);
- $message = str_replace("\n", "\r\n", $message);
-
- list($new_headers, $new_body) = explode("\r\n\r\n", $message, 2);
-
- $new_headers = preg_replace('#^To:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
- $new_headers = preg_replace('#^Subject:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
- $new_headers = preg_replace("#^MIME-Version: 1.0\r?\n#mi", '', $new_headers, 1);
- $new_headers = preg_replace('#^Content-Type:\s+' . preg_quote($headers_array['Content-Type'], '#') . "\r?\n#mi", '', $new_headers);
- $new_headers = preg_replace('#^Content-Transfer-Encoding:\s+' . preg_quote($headers_array['Content-Transfer-Encoding'], '#') . "\r?\n#mi", '', $new_headers);
-
- unlink($plaintext_file);
- unlink($ciphertext_file);
-
- if ($this->smime_sign) {
- openssl_pkey_free($senders_private_key);
- }
-
- return array($new_headers, $new_body);
- }
-
-
- /**
- * Sets the email to be encrypted with S/MIME
- *
- * @param string $recipients_smime_cert_file The file path to the PEM-encoded S/MIME certificate for the recipient
- * @return fEmail The email object, to allow for method chaining
- */
- public function encrypt($recipients_smime_cert_file)
- {
- if (!extension_loaded('openssl')) {
- throw new fEnvironmentException(
- 'S/MIME encryption was requested for an email, but the %s extension is not installed',
- 'openssl'
- );
- }
-
- if (!self::stringlike($recipients_smime_cert_file)) {
- throw new fProgrammerException(
- "The recipient's S/MIME certificate filename specified, %s, does not appear to be a valid filename",
- $recipients_smime_cert_file
- );
- }
-
- $this->smime_encrypt = TRUE;
- $this->recipients_smime_cert_file = $recipients_smime_cert_file;
- return $this;
- }
-
-
- /**
- * Extracts just the email addresses from an array of strings containing an
- * <email@address.com> or "Name" <email@address.com> combination.
- *
- * @param array $list The list of email or name/email to extract from
- * @return array The email addresses
- */
- private function extractEmails($list)
- {
- $output = array();
- foreach ($list as $email) {
- if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
- $output[] = $match[2];
- } else {
- preg_match(self::EMAIL_REGEX, $email, $match);
- $output[] = $match[0];
- }
- }
- return $output;
- }
-
-
- /**
- * Extracts the filename and mime-type from an fFile object
- *
- * @param string|fFile &$contents The file to extrapolate the info from
- * @param string &$filename The filename to use for the file
- * @param string &$mime_type The mime type of the file
- * @return void
- */
- private function extrapolateFileInfo(&$contents, &$filename, &$mime_type)
- {
- if ($contents instanceof fFile) {
- if ($filename === NULL) {
- $filename = $contents->getName();
- }
- if ($mime_type === NULL) {
- $mime_type = $contents->getMimeType();
- }
- $contents = $contents->read();
-
- } else {
- if (!self::stringlike($filename)) {
- throw new fProgrammerException(
- 'The filename specified, %s, does not appear to be a valid filename',
- $filename
- );
- }
-
- $filename = (string) $filename;
-
- if ($mime_type === NULL) {
- $mime_type = fFile::determineMimeType($filename, $contents);
- }
- }
- }
-
-
- /**
- * Generates a new filename in an attempt to create a unique name
- *
- * @param string $filename The filename to generate another name for
- * @return string The newly generated filename
- */
- private function generateNewFilename($filename)
- {
- $filename_info = fFilesystem::getPathInfo($filename);
- if (preg_match('#_copy(\d+)($|\.)#D', $filename_info['filename'], $match)) {
- $i = $match[1] + 1;
- } else {
- $i = 1;
- }
- $extension = ($filename_info['extension']) ? '.' . $filename_info['extension'] : '';
- return preg_replace('#_copy\d+$#D', '', $filename_info['filename']) . '_copy' . $i . $extension;
- }
-
-
- /**
- * Loads the plaintext version of the email body from a file and applies replacements
- *
- * The should contain either ASCII or UTF-8 encoded text. Please see
- * http://flourishlib.com/docs/UTF-8 for more information.
- *
- * @throws fValidationException When no file was specified, the file does not exist or the path specified is not a file
- *
- * @param string|fFile $file The plaintext version of the email body
- * @param array $replacements The method will search the contents of the file for each key and replace it with the corresponding value
- * @return fEmail The email object, to allow for method chaining
- */
- public function loadBody($file, $replacements=array())
- {
- if (!$file instanceof fFile) {
- $file = new fFile($file);
- }
-
- $plaintext = $file->read();
- if ($replacements) {
- $plaintext = strtr($plaintext, $replacements);
- }
-
- $this->plaintext_body = $plaintext;
- return $this;
- }
-
-
- /**
- * Loads the plaintext version of the email body from a file and applies replacements
- *
- * The should contain either ASCII or UTF-8 encoded text. Please see
- * http://flourishlib.com/docs/UTF-8 for more information.
- *
- * @throws fValidationException When no file was specified, the file does not exist or the path specified is not a file
- *
- * @param string|fFile $file The plaintext version of the email body
- * @param array $replacements The method will search the contents of the file for each key and replace it with the corresponding value
- * @return fEmail The email object, to allow for method chaining
- */
- public function loadHTMLBody($file, $replacements=array())
- {
- if (!$file instanceof fFile) {
- $file = new fFile($file);
- }
-
- $html = $file->read();
- if ($replacements) {
- $html = strtr($html, $replacements);
- }
-
- $this->html_body = $html;
- return $this;
- }
-
-
- /**
- * Encodes a string to base64
- *
- * @param string $content The content to encode
- * @return string The encoded string
- */
- private function makeBase64($content)
- {
- return chunk_split(base64_encode($content));
- }
-
-
- /**
- * Encodes a string to quoted-printable, properly handles UTF-8
- *
- * @param string $content The content to encode
- * @return string The encoded string
- */
- private function makeQuotedPrintable($content)
- {
- // Homogenize the line-endings to CRLF
- $content = str_replace("\r\n", "\n", $content);
- $content = str_replace("\r", "\n", $content);
- $content = str_replace("\n", "\r\n", $content);
-
- // A quick a dirty hex encoding
- $content = rawurlencode($content);
- $content = str_replace('=', '%3D', $content);
- $content = str_replace('%', '=', $content);
-
- // Decode characters that don't have to be coded
- $decodings = array(
- '=20' => ' ', '=21' => '!', '=22' => '"', '=23' => '#',
- '=24' => '$', '=25' => '%', '=26' => '&', '=27' => "'",
- '=28' => '(', '=29' => ')', '=2A' => '*', '=2B' => '+',
- '=2C' => ',', '=2D' => '-', '=2E' => '.', '=2F' => '/',
- '=3A' => ':', '=3B' => ';', '=3C' => '<', '=3E' => '>',
- '=3F' => '?', '=40' => '@', '=5B' => '[', '=5C' => '\\',
- '=5D' => ']', '=5E' => '^', '=5F' => '_', '=60' => '`',
- '=7B' => '{', '=7C' => '|', '=7D' => '}', '=7E' => '~'
- );
-
- $content = strtr($content, $decodings);
-
- $output = '';
-
- $length = strlen($content);
-
- // This loop goes through and ensures we are wrapping by 76 chars
- $line_length = 0;
- for ($i=0; $i<$length; $i++) {
-
- // Get info about the next character
- $char_length = ($content[$i] == '=') ? 3 : 1;
- $char = $content[$i];
- if ($char_length == 3) {
- $char .= $content[$i+1] . $content[$i+2];
- }
-
- // Skip characters if we have an encoded character, this must be
- // done before checking for whitespace at the beginning and end of
- // lines or else characters in the content will be skipped
- $i += $char_length-1;
-
- // Spaces and tabs at the beginning and ending of lines have to be encoded
- $begining_or_end = $line_length > 69 || $line_length == 0;
- $tab_or_space = $char == ' ' || $char == "\t";
- if ($begining_or_end && $tab_or_space) {
- $char_length = 3;
- $char = ($char == ' ') ? '=20' : '=09';
- }
-
- // If we have too long a line, wrap it
- if ($char != "\r" && $char != "\n" && $line_length + $char_length > 75) {
- $output .= "=\r\n";
- $line_length = 0;
- }
-
- // Add the character
- $output .= $char;
-
- // Figure out how much longer the line is now
- if ($char == "\r" || $char == "\n") {
- $line_length = 0;
- } else {
- $line_length += $char_length;
- }
- }
-
- return $output;
- }
-
-
- /**
- * Sends the email
- *
- * The return value is the message id, which should be included as the
- * `Message-ID` header of the email. While almost all SMTP servers will not
- * modify this value, testing has indicated at least one (smtp.live.com
- * for Windows Live Mail) does.
- *
- * @throws fValidationException When ::validate() throws an exception
- *
- * @param fSMTP $connection The SMTP connection to send the message over
- * @return string The message id for the message - see method description for details
- */
- public function send($connection=NULL)
- {
- $this->validate();
-
- // The mail() function on Windows doesn't support names in headers so
- // we must strip them down to just the email address
- if ($connection === NULL && fCore::checkOS('windows')) {
- $vars = array('bcc_emails', 'bounce_to_email', 'cc_emails', 'from_email', 'reply_to_email', 'sender_email', 'to_emails');
- foreach ($vars as $var) {
- if (!is_array($this->$var)) {
- if (preg_match(self::NAME_EMAIL_REGEX, $this->$var, $match)) {
- $this->$var = $match[2];
- }
- } else {
- $new_emails = array();
- foreach ($this->$var as $email) {
- if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
- $email = $match[2];
- }
- $new_emails[] = $email;
- }
- $this->$var = $new_emails;
- }
- }
- }
-
- $to = substr(trim($this->buildMultiAddressHeader("To", $this->to_emails)), 4);
-
- $top_level_boundary = $this->createBoundary();
- $headers = $this->createHeaders($top_level_boundary, $this->message_id);
-
- $subject = str_replace(array("\r", "\n"), '', $this->subject);
- $subject = self::makeEncodedWord($subject, 9);
-
- $body = $this->createBody($top_level_boundary);
-
- if ($this->smime_encrypt || $this->smime_sign) {
- list($headers, $body) = $this->createSMIMEBody($to, $subject, $headers, $body);
- }
-
- // Remove extra line breaks
- $headers = trim($headers);
- $body = trim($body);
-
- if ($connection) {
- $to_emails = $this->extractEmails($this->to_emails);
- $to_emails = array_merge($to_emails, $this->extractEmails($this->cc_emails));
- $to_emails = array_merge($to_emails, $this->extractEmails($this->bcc_emails));
- $from = $this->bounce_to_email ? $this->bounce_to_email : current($this->extractEmails(array($this->from_email)));
- $connection->send($from, $to_emails, "To: " . $to . "\r\nSubject: " . $subject . "\r\n" . $headers, $body);
- return $this->message_id;
- }
-
- // Sendmail when not in safe mode will allow you to set the envelope from address via the -f parameter
- $parameters = NULL;
- if (!fCore::checkOS('windows') && $this->bounce_to_email) {
- preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
- $parameters = '-f ' . $matches[0];
-
- // Windows takes the Return-Path email from the sendmail_from ini setting
- } elseif (fCore::checkOS('windows') && $this->bounce_to_email) {
- $old_sendmail_from = ini_get('sendmail_from');
- preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
- ini_set('sendmail_from', $matches[0]);
- }
-
- // This is a gross qmail fix that is a last resort
- if (self::$popen_sendmail || self::$convert_crlf) {
- $to = str_replace("\r\n", "\n", $to);
- $subject = str_replace("\r\n", "\n", $subject);
- $body = str_replace("\r\n", "\n", $body);
- $headers = str_replace("\r\n", "\n", $headers);
- }
-
- // If the user is using qmail and wants to try to fix the \r\r\n line break issue
- if (self::$popen_sendmail) {
- $sendmail_command = ini_get('sendmail_path');
- if ($parameters) {
- $sendmail_command .= ' ' . $parameters;
- }
-
- $sendmail_process = popen($sendmail_command, 'w');
- fprintf($sendmail_process, "To: %s\n", $to);
- fprintf($sendmail_process, "Subject: %s\n", $subject);
- if ($headers) {
- fprintf($sendmail_process, "%s\n", $headers);
- }
- fprintf($sendmail_process, "\n%s\n", $body);
- $error = pclose($sendmail_process);
-
- // This is the normal way to send mail
- } else {
- // On Windows, mail() sends directly to an SMTP server and will
- // strip a leading . from the body
- if (fCore::checkOS('windows')) {
- $body = preg_replace('#^\.#', '..', $body);
- }
-
- if ($parameters) {
- $error = !mail($to, $subject, $body, $headers, $parameters);
- } else {
- $error = !mail($to, $subject, $body, $headers);
- }
- }
-
- if (fCore::checkOS('windows') && $this->bounce_to_email) {
- ini_set('sendmail_from', $old_sendmail_from);
- }
-
- if ($error) {
- throw new fConnectivityException(
- 'An error occured while trying to send the email entitled %s',
- $this->subject
- );
- }
-
- return $this->message_id;
- }
-
-
- /**
- * Sets the plaintext version of the email body
- *
- * This method accepts either ASCII or UTF-8 encoded text. Please see
- * http://flourishlib.com/docs/UTF-8 for more information.
- *
- * @param string $plaintext The plaintext version of the email body
- * @param boolean $unindent_expand_constants If this is `TRUE`, the body will be unindented as much as possible and {CONSTANT_NAME} will be replaced with the value of the constant
- * @return fEmail The email object, to allow for method chaining
- */
- public function setBody($plaintext, $unindent_expand_constants=FALSE)
- {
- if ($unindent_expand_constants) {
- $plaintext = self::unindentExpand($plaintext);
- }
-
- $this->plaintext_body = $plaintext;
- return $this;
- }
-
-
- /**
- * Adds the email address the email will be bounced to
- *
- * This email address will be set to the `Return-Path` header.
- *
- * @param string $email The email address to bounce to
- * @return fEmail The email object, to allow for method chaining
- */
- public function setBounceToEmail($email)
- {
- if (ini_get('safe_mode') && !fCore::checkOS('windows')) {
- throw new fProgrammerException('It is not possible to set a Bounce-To Email address when safe mode is enabled on a non-Windows server');
- }
- if (!$email) {
- return;
- }
-
- $this->bounce_to_email = self::combineNameEmail('', $email);
- return $this;
- }
-
-
- /**
- * Adds the `From:` email address to the email
- *
- * @param string $email The email address being sent from
- * @param string $name The from email user's name - unfortunately on windows this is ignored
- * @return fEmail The email object, to allow for method chaining
- */
- public function setFromEmail($email, $name=NULL)
- {
- if (!$email) {
- return;
- }
-
- $this->from_email = self::combineNameEmail($name, $email);
- return $this;
- }
-
-
- /**
- * Sets the HTML version of the email body
- *
- * This method accepts either ASCII or UTF-8 encoded text. Please see
- * http://flourishlib.com/docs/UTF-8 for more information.
- *
- * @param string $html The HTML version of the email body
- * @return fEmail The email object, to allow for method chaining
- */
- public function setHTMLBody($html)
- {
- $this->html_body = $html;
- return $this;
- }
-
-
- /**
- * Adds the `Reply-To:` email address to the email
- *
- * @param string $email The email address to reply to
- * @param string $name The reply-to email user's name
- * @return fEmail The email object, to allow for method chaining
- */
- public function setReplyToEmail($email, $name=NULL)
- {
- if (!$email) {
- return;
- }
-
- $this->reply_to_email = self::combineNameEmail($name, $email);
- return $this;
- }
-
-
- /**
- * Adds the `Sender:` email address to the email
- *
- * The `Sender:` header is used to indicate someone other than the `From:`
- * address is actually submitting the message to the network.
- *
- * @param string $email The email address the message is actually being sent from
- * @param string $name The sender email user's name
- * @return fEmail The email object, to allow for method chaining
- */
- public function setSenderEmail($email, $name=NULL)
- {
- if (!$email) {
- return;
- }
-
- $this->sender_email = self::combineNameEmail($name, $email);
- return $this;
- }
-
-
- /**
- * Sets the subject of the email
- *
- * This method accepts either ASCII or UTF-8 encoded text. Please see
- * http://flourishlib.com/docs/UTF-8 for more information.
- *
- * @param string $subject The subject of the email
- * @return fEmail The email object, to allow for method chaining
- */
- public function setSubject($subject)
- {
- $this->subject = $subject;
- return $this;
- }
-
-
- /**
- * Sets the email to be signed with S/MIME
- *
- * @param string $senders_smime_cert_file The file path to the sender's PEM-encoded S/MIME certificate
- * @param string $senders_smime_pk_file The file path to the sender's S/MIME private key
- * @param string $senders_smime_pk_password The password for the sender's S/MIME private key
- * @return fEmail The email object, to allow for method chaining
- */
- public function sign($senders_smime_cert_file, $senders_smime_pk_file, $senders_smime_pk_password)
- {
- if (!extension_loaded('openssl')) {
- throw new fEnvironmentException(
- 'An S/MIME signature was requested for an email, but the %s extension is not installed',
- 'openssl'
- );
- }
-
- if (!self::stringlike($senders_smime_cert_file)) {
- throw new fProgrammerException(
- "The sender's S/MIME certificate file specified, %s, does not appear to be a valid filename",
- $senders_smime_cert_file
- );
- }
- if (!file_exists($senders_smime_cert_file) || !is_readable($senders_smime_cert_file)) {
- throw new fEnvironmentException(
- "The sender's S/MIME certificate file specified, %s, does not exist or could not be read",
- $senders_smime_cert_file
- );
- }
-
- if (!self::stringlike($senders_smime_pk_file)) {
- throw new fProgrammerException(
- "The sender's S/MIME primary key file specified, %s, does not appear to be a valid filename",
- $senders_smime_pk_file
- );
- }
- if (!file_exists($senders_smime_pk_file) || !is_readable($senders_smime_pk_file)) {
- throw new fEnvironmentException(
- "The sender's S/MIME primary key file specified, %s, does not exist or could not be read",
- $senders_smime_pk_file
- );
- }
-
- $this->smime_sign = TRUE;
- $this->senders_smime_cert_file = $senders_smime_cert_file;
- $this->senders_smime_pk_file = $senders_smime_pk_file;
- $this->senders_smime_pk_password = $senders_smime_pk_password;
- return $this;
- }
-
-
- /**
- * Validates that all of the parts of the email are valid
- *
- * @throws fValidationException When part of the email is missing or formatted incorrectly
- *
- * @return void
- */
- private function validate()
- {
- $validation_messages = array();
-
- // Check all multi-address email field
- $multi_address_field_list = array(
- 'to_emails' => self::compose('recipient'),
- 'cc_emails' => self::compose('CC recipient'),
- 'bcc_emails' => self::compose('BCC recipient')
- );
-
- foreach ($multi_address_field_list as $field => $name) {
- foreach ($this->$field as $email) {
- if ($email && !preg_match(self::NAME_EMAIL_REGEX, $email) && !preg_match(self::EMAIL_REGEX, $email)) {
- $validation_messages[] = htmlspecialchars(self::compose(
- 'The %1$s %2$s is not a valid email address. Should be like "John Smith" <name@example.com> or name@example.com.',
- $name,
- $email
- ), ENT_QUOTES, 'UTF-8');
- }
- }
- }
-
- // Check all single-address email fields
- $single_address_field_list = array(
- 'from_email' => self::compose('From email address'),
- 'reply_to_email' => self::compose('Reply-To email address'),
- 'sender_email' => self::compose('Sender email address'),
- 'bounce_to_email' => self::compose('Bounce-To email address')
- );
-
- foreach ($single_address_field_list as $field => $name) {
- if ($this->$field && !preg_match(self::NAME_EMAIL_REGEX, $this->$field) && !preg_match(self::EMAIL_REGEX, $this->$field)) {
- $validation_messages[] = htmlspecialchars(self::compose(
- 'The %1$s %2$s is not a valid email address. Should be like "John Smith" <name@example.com> or name@example.com.',
- $name,
- $this->$field
- ), ENT_QUOTES, 'UTF-8');
- }
- }
-
- // Make sure the required fields are all set
- if (!$this->to_emails) {
- $validation_messages[] = self::compose(
- "Please provide at least one recipient"
- );
- }
-
- if (!$this->from_email) {
- $validation_messages[] = self::compose(
- "Please provide the from email address"
- );
- }
-
- if (!self::stringlike($this->subject)) {
- $validation_messages[] = self::compose(
- "Please provide an email subject"
- );
- }
-
- if (strpos($this->subject, "\n") !== FALSE) {
- $validation_messages[] = self::compose(
- "The subject contains one or more newline characters"
- );
- }
-
- if (!self::stringlike($this->plaintext_body)) {
- $validation_messages[] = self::compose(
- "Please provide a plaintext email body"
- );
- }
-
- // Make sure the attachments look good
- foreach ($this->attachments as $filename => $file_info) {
- if (!self::stringlike($file_info['mime-type'])) {
- $validation_messages[] = self::compose(
- "No mime-type was specified for the attachment %s",
- $filename
- );
- }
- if (!self::stringlike($file_info['contents'])) {
- $validation_messages[] = self::compose(
- "The attachment %s appears to be a blank file",
- $filename
- );
- }
- }
-
- if ($validation_messages) {
- throw new fValidationException(
- 'The email could not be sent because:',
- $validation_messages
- );
- }
- }
- }
- /**
- * Copyright (c) 2008-2011 Will Bond <will@flourishlib.com>, others
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */