Index: trunk/core/kernel/utility/email_send.php =================================================================== diff -u -N --- trunk/core/kernel/utility/email_send.php (revision 8402) +++ trunk/core/kernel/utility/email_send.php (revision 0) @@ -1,1968 +0,0 @@ -guessOptions = Array ( - 'attachments' => Array(), - 'inline_attachments' => Array (), - 'text_part' => false, - 'html_part' => false, - ); - - // read SMTP server connection params from config - $smtp_mapping = Array ('server' => 'Smtp_Server', 'port' => 'Smtp_Port'); - if ($this->Application->ConfigValue('Smtp_Authenticate')) { - $smtp_mapping['username'] = 'Smtp_User'; - $smtp_mapping['password'] = 'Smtp_Pass'; - } - - foreach ($smtp_mapping as $smtp_name => $config_name) { - $this->smtpParams[$smtp_name] = $this->Application->ConfigValue($config_name); - } - $this->smtpParams['use_auth'] = isset($this->smtpParams['username']) ? true : false; - $this->smtpParams['localhost'] = 'localhost'; // The value to give when sending EHLO or HELO. - - $this->sendMethod = $this->smtpParams['server'] && $this->smtpParams['port'] ? 'SMTP' : 'Mail'; - - if ($this->sendMethod == 'SMTP') { - // create connection object if we will use SMTP - $this->smtpSocket =& $this->Application->makeClass('Socket'); - } - - $this->SetCharset(null, true); - } - - - /** - * Returns new message id header by sender's email address - * - * @param string $email_address email address - * @return string - */ - function GenerateMessageID($email_address) - { - list ($micros, $seconds) = explode(' ', microtime()); - list ($user, $domain) = explode('@', $email_address, 2); - - $message_id = strftime('%Y%m%d%H%M%S', $seconds).substr($micros, 1, 5).'.'.preg_replace('/[^A-Za-z]+/', '-', $user).'@'.$domain; - - $this->SetHeader('Message-ID', '<'.$message_id.'>'); - } - - /** - * Returns extension of given filename - * - * @param string $filename - * @return string - */ - function GetFilenameExtension($filename) - { - $last_dot = strrpos($filename, '.'); - return $last_dot !== false ? substr($filename, $last_dot + 1) : ''; - } - - /** - * Creates boundary for part by number (only if it's missing) - * - * @param int $part_number - * - */ - - function CreatePartBoundary($part_number) - { - $part =& $this->parts[$part_number]; - if (!isset($part['BOUNDARY'])) { - $part['BOUNDARY'] = md5(uniqid($part_number.time())); - - } - } - - /** - * Returns ready to use headers associative array of any message part by it's number - * - * @param int $part_number - * @return Array - */ - function GetPartHeaders($part_number) - { - $part =& $this->parts[$part_number]; - - if (!isset($part['Content-Type'])) { - return $this->SetError('MISSING_CONTENT_TYPE'); - } - - $full_type = strtolower($part['Content-Type']); - list ($type, $sub_type) = explode('/', $full_type); - - $headers['Content-Type'] = $full_type; - switch ($type) { - case 'text': - case 'image': - case 'audio': - case 'video': - case 'application': - case 'message': - // 1. update content-type header - if (isset($part['CHARSET'])) { - $headers['Content-Type'] .= '; charset='.$part['CHARSET']; - } - if (isset($part['NAME'])) { - $headers['Content-Type'] .= '; name="'.$part['NAME'].'"'; - } - - // 2. set content-transfer-encoding header - if (isset($part['Content-Transfer-Encoding'])) { - $headers['Content-Transfer-Encoding'] = $part['Content-Transfer-Encoding']; - } - - // 3. set content-disposition header - if (isset($part['DISPOSITION']) && $part['DISPOSITION']) { - $headers['Content-Disposition'] = $part['DISPOSITION']; - if (isset($part['NAME'])) { - $headers['Content-Disposition'] .= '; filename="'.$part['NAME'].'"'; - } - } - break; - - case 'multipart': - switch ($sub_type) { - case 'alternative': - case 'related': - case 'mixed': - case 'parallel': - $this->CreatePartBoundary($part_number); - $headers['Content-Type'] .= '; boundary="'.$part['BOUNDARY'].'"'; - break; - - default: - return $this->SetError('INVALID_MULTIPART_SUBTYPE', Array($sub_type)); - } - break; - - default: - return $this->SetError('INVALID_CONTENT_TYPE', Array($full_type)); - } - - // set content-id if any - if (isset($part['Content-ID'])) { - $headers['Content-ID'] = '<'.$part['Content-ID'].'>'; - } - - return $headers; - } - - function GetPartBody($part_number) - { - $part =& $this->parts[$part_number]; - - if (!isset($part['Content-Type'])) { - return $this->SetError('MISSING_CONTENT_TYPE'); - } - - $full_type = strtolower($part['Content-Type']); - list ($type, $sub_type) = explode('/', $full_type); - - $body = ''; - switch ($type) { - // compose text/binary content - case 'text': - case 'image': - case 'audio': - case 'video': - case 'application': - case 'message': - // 1. get content of part - if (isset($part['FILENAME'])) { - // content provided via absolute path to content containing file - $filename = $part['FILENAME']; - $file_size = filesize($filename); - - $body = file_get_contents($filename); - if ($body === false) { - return $this->SetError('FILE_PART_OPEN_ERROR', Array($filename)); - } - - $actual_size = strlen($body); - if (($file_size === false || $actual_size > $file_size) && get_magic_quotes_runtime()) { - $body = stripslashes($body); - } - - if ($file_size !== false && $actual_size != $file_size) { - return $this->SetError('FILE_PART_DATA_ERROR', Array($filename)); - } - } - else { - // content provided directly as one of part keys - if (!isset($part['DATA'])) { - return $this->SetError('FILE_PART_DATA_MISSING'); - } - $body =& $part['DATA']; - } - - // 2. get part transfer encoding - $encoding = isset($part['Content-Transfer-Encoding']) ? strtolower($part['Content-Transfer-Encoding']) : ''; - if (!in_array($encoding, Array ('', 'base64', 'quoted-printable', '7bit'))) { - return $this->SetError('INVALID_ENCODING', Array($encoding)); - } - - if ($encoding == 'base64') { - // split base64 encoded text by 76 symbols at line (MIME requirement) - $body = chunk_split( base64_encode($body) ); - } - break; - - case 'multipart': - // compose multipart message - switch ($sub_type) { - case 'alternative': - case 'related': - case 'mixed': - case 'parallel': - $this->CreatePartBoundary($part_number); - $boundary = $this->line_break.'--'.$part['BOUNDARY']; - - foreach ($part['PARTS'] as $multipart_number) { - $body .= $boundary.$this->line_break; - $part_headers = $this->GetPartHeaders($multipart_number); - if ($part_headers === false) { - // some of sub-part headers were invalid - return false; - } - - foreach ($part_headers as $header_name => $header_value) { - $body .= $header_name.': '.$header_value.$this->line_break; - } - - $part_body = $this->GetPartBody($multipart_number); - if ($part_body === false) { - // part body was invalid - return false; - } - - $body .= $this->line_break.$part_body; - } - $body .= $boundary.'--'.$this->line_break; - break; - - default: - return $this->SetError('INVALID_MULTIPART_SUBTYPE', Array($sub_type)); - } - break; - default: - return $this->SetError('INVALID_CONTENT_TYPE', Array($full_type)); - } - - return $body; - } - - /** - * Applies quoted-printable encoding to specified text - * - * @param string $text - * @param string $header_charset - * @param int $break_lines - * @return unknown - */ - function QuotedPrintableEncode($text, $header_charset = '', $break_lines = 1) - { - $ln = strlen($text); - $h = strlen($header_charset) > 0; - if ($h) { - $s = Array ( - '=' => 1, - '?' => 1, - '_' => 1, - '(' => 1, - ')' => 1, - '<' => 1, - '>' => 1, - '@' => 1, - ',' => 1, - ';' => 1, - '"' => 1, - '\\' => 1, - /* - '/' => 1, - '[' => 1, - ']' => 1, - ':' => 1, - '.' => 1, - */ - ); - - $b = $space = $break_lines = 0; - for ($i = 0; $i < $ln; $i++) { - if (isset($s[$text[$i]])) { - $b = 1; - break; - } - - switch ($o = ord($text[$i])) { - case 9: - case 32: - $space = $i + 1; - $b = 1; - break 2; - case 10: - case 13: - break 2; - default: - if ($o < 32 || $o > 127) { - $b = 1; - break 2; - } - } - } - - if($i == $ln) { - return $text; - } - - if ($space > 0) { - return substr($text, 0, $space).($space < $ln ? $this->QuotedPrintableEncode(substr($text, $space), $header_charset, 0) : ''); - } - } - - for ($w = $e = '', $n = 0, $l = 0, $i = 0; $i < $ln; $i++) { - $c = $text[$i]; - $o = ord($c); - $en = 0; - switch ($o) { - case 9: - case 32: - if (!$h) { - $w = $c; - $c = ''; - } - else { - if ($b) { - if ($o == 32) { - $c = '_'; - } - else { - $en = 1; - } - } - } - break; - case 10: - case 13: - if (strlen($w)) { - if ($break_lines && $l + 3 > 75) { - $e .= '='.$this->line_break; - $l = 0; - } - - $e .= sprintf('=%02X', ord($w)); - $l += 3; - $w = ''; - } - - $e .= $c; - if ($h) { - $e .= "\t"; - } - $l = 0; - continue 2; - case 46: - case 70: - case 102: - $en = (!$h && ($l == 0 || $l + 1 > 75)); - break; - default: - if ($o > 127 || $o < 32 || !strcmp($c, '=')) { - $en = 1; - } - elseif ($h && isset($s[$c])) { - $en = 1; - } - break; - } - - if (strlen($w)) { - if ($break_lines && $l + 1 > 75) { - $e .= '='.$this->line_break; - $l = 0; - } - $e .= $w; - $l++; - $w = ''; - } - - if (strlen($c)) { - if ($en) { - $c = sprintf('=%02X', $o); - $el = 3; - $n = 1; - $b = 1; - } - else { - $el = 1; - } - if ($break_lines && $l + $el > 75) { - $e .= '='.$this->line_break; - $l = 0; - } - $e .= $c; - $l += $el; - } - } - if (strlen($w)) { - if ($break_lines && $l + 3 > 75) { - $e .= '='.$this->line_break; - } - $e .= sprintf('=%02X', ord($w)); - } - - return $h && $n ? '=?'.$header_charset.'?q?'.$e.'?=' : $e; - } - - /** - * Sets message header + encodes is by quoted-printable using charset specified - * - * @param string $name - * @param string $value - * @param string $encoding_charset - */ - function SetHeader($name, $value, $encoding_charset = '') - { - if ($encoding_charset) { - // actually for headers base64 method may give shorter result - $value = $this->QuotedPrintableEncode($value, $encoding_charset); - } - - $this->headers[$name] = $value; - } - - /** - * Sets header + automatically encodes it using default charset - * - * @param string $name - * @param string $value - */ - function SetEncodedHeader($name, $value) - { - $this->SetHeader($name, $value, $this->charset); - } - - /** - * Sets header which value is email and username +autoencode - * - * @param string $header - * @param string $address - * @param string $name - */ - function SetEncodedEmailHeader($header, $address, $name) - { - $this->SetHeader($header, $this->QuotedPrintableEncode($name, $this->charset).' <'.$address.'>'); - } - - function SetMultipleEncodedEmailHeader($header, $addresses) - { - $value = ''; - foreach ($addresses as $name => $address) { - $value .= $this->QuotedPrintableEncode($name, $this->charset).' <'.$address.'>, '; - } - $value = preg_replace('/(.*),$/', '\\1', $value); - - $this->SetHeader($header, $value); - } - - - /** - * Adds new part to message and returns it's number - * - * @param Array $part_definition - * @param int $part_number number of new part - * @return int - */ - function AddPart(&$part_definition, $part_number = false) - { - $part_number = $part_number !== false ? $part_number : count($this->parts); - $this->parts[$part_number] =& $part_definition; - return $part_number; - } - - /** - * Returns text version of HTML document - * - * @param string $html - * @return string - */ - function ConvertToText($html) - { - $search = Array ( - "'(<\/td>.*)[\r\n]+(.*[\r\n]{0,2})|(<\/p>)|(<\/div>)|(<\/tr>)'i", - "'(.*?)'si", - "''si", - "'(.*?)'si", - "''si", -// "'^[\s\n\r\t]+'", //strip all spacers & newlines in the begin of document -// "'[\s\n\r\t]+$'", //strip all spacers & newlines in the end of document - "'&(quot|#34);'i", - "'&(amp|#38);'i", - "'&(lt|#60);'i", - "'&(gt|#62);'i", - "'&(nbsp|#160);'i", - "'&(iexcl|#161);'i", - "'&(cent|#162);'i", - "'&(pound|#163);'i", - "'&(copy|#169);'i", - "'&#(\d+);'e" - ); - - $replace = Array ( - "\\1\t\\2", - "\n", - "", - "", - "", - "", -// "", -// "", - "\"", - "&", - "<", - ">", - " ", - chr(161), - chr(162), - chr(163), - chr(169), - "chr(\\1)" - ); - - return strip_tags( preg_replace ($search, $replace, $html) ); - } - - /** - * Add text OR html part to message (optionally encoded) - * - * @param string $text part's text - * @param bool $is_html this html part or not - * @param bool $encode encode message using quoted-printable encoding - * - * @return int number of created part - */ - function CreateTextHtmlPart($text, $is_html = false, $encode = true) - { - if ($is_html) { - // if adding HTML part, then create plain-text part too - $this->CreateTextHtmlPart($this->ConvertToText($text)); - } - - // in case if text is from $_REQUEST, then line endings are "\r\n", but we need "\n" here - - $text = str_replace("\r\n", "\n", $text); // possible case - $text = str_replace("\r", "\n", $text); // impossible case, but just in case replace this too - - $definition = Array ( - 'Content-Type' => $is_html ? 'text/html' : 'text/plain', - 'CHARSET' => $this->charset, - 'DATA' => $encode ? $this->QuotedPrintableEncode($text) : $text, - ); - - if ($encode) { - $definition['Content-Transfer-Encoding'] = 'quoted-printable'; - } - - $guess_name = $is_html ? 'html_part' : 'text_part'; - $part_number = $this->guessOptions[$guess_name] !== false ? $this->guessOptions[$guess_name] : false; - - $part_number = $this->AddPart($definition, $part_number); - $this->guessOptions[$guess_name] = $part_number; - - return $part_number; - } - - /** - * Adds attachment part to message - * - * @param string $file name of the file with attachment body - * @param string $attach_name name for attachment (name of file is used, when not specified) - * @param string $content_type content type for attachment - * @param string $content body of file to be attached - * @param bool $inline is attachment inline or not - * - * @return int number of created part - */ - function AddAttachment($file = '', $attach_name = '', $content_type = '', $content = '', $inline = false) - { - $definition = Array ( - 'Disposition' => $inline ? 'inline' : 'attachment', - 'Content-Type' => $content_type ? $content_type : 'automatic/name', - ); - - if ($file) { - // filename of attachment given - $definition['FileName'] = $file; - } - - if ($attach_name) { - // name of attachment given - $definition['Name'] = $attach_name; - } - - if ($content) { - // attachment data is given - $definition['Data'] = $content; - } - - $definition =& $this->GetFileDefinition($definition); - $part_number = $this->AddPart($definition); - - if ($inline) { - // it's inline attachment and needs content-id to be addressed by in message - $this->parts[$part_number]['Content-ID'] = md5(uniqid($part_number.time())).'.'.$this->GetFilenameExtension($attach_name ? $attach_name : $file); - } - - $this->guessOptions[$inline ? 'inline_attachments' : 'attachments'][] = $part_number; - - return $part_number; - } - - /** - * Adds another MIME message as attachment to message being composed - * - * @param string $file name of the file with attachment body - * @param string $attach_name name for attachment (name of file is used, when not specified) - * @param string $content body of file to be attached - * - * @return int number of created part - */ - function AddMessageAttachment($file = '', $attach_name = '', $content = '') - { - $part_number = $this->AddAttachment($file, $attach_name, 'message/rfc822', $content, true); - unset($this->parts[$part_number]['Content-ID']); // messages don't have content-id, but have inline disposition - return $part_number; - } - - /** - * Creates multipart of specified type and returns it's number - * - * @param Array $part_numbers - * @param string $multipart_type = {alternative,related,mixed,paralell} - * @return int - */ - function CreateMultipart($part_numbers, $multipart_type) - { - $types = Array ('alternative', 'related' , 'mixed', 'paralell'); - if (!in_array($multipart_type, $types)) { - return $this->SetError('INVALID_MULTIPART_SUBTYPE', Array($multipart_type)); - } - - $definition = Array ( - 'Content-Type' => 'multipart/'.$multipart_type, - 'PARTS' => $part_numbers, - ); - - return $this->AddPart($definition); - } - - /** - * Creates missing content-id header for inline attachments - * - * @param int $part_number - */ - function CreateContentID($part_number) - { - $part =& $this->parts[$part_number]; - if (!isset($part['Content-ID']) && $part['DISPOSITION'] == 'inline') { - $part['Content-ID'] = md5(uniqid($part_number.time())).'.'.$this->GetFilenameExtension($part['NAME']); - } - } - - /** - * Returns attachment part based on file used in attachment - * - * @param Array $file - * @return Array - */ - function &GetFileDefinition ($file) - { - $name = ''; - if (isset($file['Name'])) { - // if name is given directly, then use it - $name = $file['Name']; - } - else { - // auto-guess attachment name based on source filename - $name = isset($file['FileName']) ? basename($file['FileName']) : ''; - } - - if (!$name || (!isset($file['FileName']) && !isset($file['Data']))) { - // filename not specified || no filename + no direct file content - return $this->SetError('MISSING_FILE_DATA'); - } - - $encoding = 'base64'; - if (isset($file['Content-Type'])) { - $content_type = $file['Content-Type']; - list ($type, $sub_type) = explode('/', $content_type); - - switch ($type) { - case 'text': - case 'image': - case 'audio': - case 'video': - case 'application': - break; - - case 'message': - $encoding = '7bit'; - break; - - case 'automatic': - if (!$name) { - return $this->SetError('MISSING_FILE_NAME'); - } - $this->guessContentType($name, $content_type, $encoding); - break; - - default: - return $this->SetError('INVALID_CONTENT_TYPE', Array($content_type)); - } - } - else { - // encoding not passed in file part, then assume, that it's binary - $content_type = 'application/octet-stream'; - } - - $definition = Array ( - 'Content-Type' => $content_type, - 'Content-Transfer-Encoding' => $encoding, - 'NAME' => $name, // attachment name - ); - - if (isset($file['Disposition'])) { - $disposition = strtolower($file['Disposition']); - if ($disposition == 'inline' || $disposition == 'attachment') { - // valid disposition header value - $definition['DISPOSITION'] = $file['Disposition']; - } - else { - return $this->SetError('INVALID_DISPOSITION', Array($file['Disposition'])); - } - } - - if (isset($file['FileName'])) { - $definition['FILENAME'] = $file['FileName']; - } - elseif (isset($file['Data'])) { - $definition['DATA'] =& $file['Data']; - } - - return $definition; - } - - /** - * Returns content-type based on filename extension - * - * @param string $filename - * @param string $content_type - * @param string $encoding - * - * @todo Regular expression used is not completely finished, that's why if extension used for - * comparing in some other extension (from list) part, that partial match extension will be returned. - * Because of two extension that begins with same 2 letters always belong to same content type - * this unfinished regular expression still gives correct result in any case. - */ - function guessContentType($filename, &$content_type, &$encoding) - { - $file_extension = strtolower( $this->GetFilenameExtension($filename) ); - - $mapping = '(xls:application/excel)(hqx:application/macbinhex40)(doc,dot,wrd:application/msword)(pdf:application/pdf) - (pgp:application/pgp)(ps,eps,ai:application/postscript)(ppt:application/powerpoint)(rtf:application/rtf) - (tgz,gtar:application/x-gtar)(gz:application/x-gzip)(php,php3:application/x-httpd-php)(js:application/x-javascript) - (ppd,psd:application/x-photoshop)(swf,swc,rf:application/x-shockwave-flash)(tar:application/x-tar)(zip:application/zip) - (mid,midi,kar:audio/midi)(mp2,mp3,mpga:audio/mpeg)(ra:audio/x-realaudio)(wav:audio/wav)(bmp:image/bitmap)(bmp:image/bitmap) - (gif:image/gif)(iff:image/iff)(jb2:image/jb2)(jpg,jpe,jpeg:image/jpeg)(jpx:image/jpx)(png:image/png)(tif,tiff:image/tiff) - (wbmp:image/vnd.wap.wbmp)(xbm:image/xbm)(css:text/css)(txt:text/plain)(htm,html:text/html)(xml:text/xml) - (mpg,mpe,mpeg:video/mpeg)(qt,mov:video/quicktime)(avi:video/x-ms-video)(eml:message/rfc822)'; - - if (preg_match('/[\(,]'.$file_extension.'[,]{0,1}.*?:(.*?)\)/s', $mapping, $regs)) { - if ($file_extension == 'eml') { - $encoding = '7bit'; - } - $content_type = $regs[1]; - } - else { - $content_type = 'application/octet-stream'; - } - } - - /** - * Using guess options combines all added parts together and returns combined part number - * - * @return int - */ - function PrepareMessageBody() - { - if ($this->bodyPartNumber === false) { - $part_number = false; // number of generated body part - - // 1. set text content of message - if ($this->guessOptions['text_part'] !== false && $this->guessOptions['html_part'] !== false) { - // text & html parts present -> compose into alternative part - $parts = Array ( - $this->guessOptions['text_part'], - $this->guessOptions['html_part'], - ); - $part_number = $this->CreateMultipart($parts, 'alternative'); - } - elseif ($this->guessOptions['text_part'] !== false) { - // only text part is defined, then leave as is - $part_number = $this->guessOptions['text_part']; - } - - if ($part_number === false) { - return $this->SetError('MESSAGE_TEXT_MISSING'); - } - - // 2. if inline attachments found, then create related multipart from text & inline attachments - if ($this->guessOptions['inline_attachments']) { - $parts = array_merge(Array($part_number), $this->guessOptions['inline_attachments']); - $part_number = $this->CreateMultipart($parts, 'related'); - } - - // 3. if normal attachments found, then create mixed multipart from text & attachments - if ($this->guessOptions['attachments']) { - $parts = array_merge(Array($part_number), $this->guessOptions['attachments']); - $part_number = $this->CreateMultipart($parts, 'mixed'); - } - - $this->bodyPartNumber = $part_number; - } - - return $this->bodyPartNumber; - } - - /** - * Returns message headers and body part (by reference in parameters) - * - * @param Array $message_headers - * @param string $message_body - * @return bool - */ - function GetHeadersAndBody(&$message_headers, &$message_body) - { - $part_number = $this->PrepareMessageBody(); - if ($part_number === false) { - return $this->SetError('MESSAGE_COMPOSE_ERROR'); - } - - $message_headers = $this->GetPartHeaders($part_number); - - // join message headers and body headers - $message_headers = array_merge_recursive2($this->headers, $message_headers); - - $message_headers['MIME-Version'] = '1.0'; - if ($this->mailerName) { - $message_headers['X-Mailer'] = $this->mailerName; - } - - $this->GenerateMessageID($message_headers['From']); - $valid_headers = $this->ValidateHeaders($message_headers); - if ($valid_headers) { - // set missing headers from existing - $from_headers = Array ('Reply-To', 'Errors-To'); - foreach ($from_headers as $header_name) { - if (!isset($message_headers[$header_name])) { - $message_headers[$header_name] = $message_headers['From']; - } - } - - $message_body = $this->GetPartBody($part_number); - return true; - } - - return false; - } - - /** - * Checks that all required headers are set and not empty - * - * @param Array $message_headers - * @return bool - */ - function ValidateHeaders($message_headers) - { - $from = isset($message_headers['From']) ? $message_headers['From'] : ''; - if (!$from) { - return $this->SetError('HEADER_MISSING', Array('From')); - } - - if (!isset($message_headers['To'])) { - return $this->SetError('HEADER_MISSING', Array('To')); - } - - if (!isset($message_headers['Subject'])) { - return $this->SetError('HEADER_MISSING', Array('Subject')); - } - - return true; - } - - /** - * Returns full message source (headers + body) for sending to SMTP server - * - * @return string - */ - function GetMessage() - { - $composed = $this->GetHeadersAndBody($message_headers, $message_body); - if ($composed) { - // add headers to resulting message - $message = ''; - foreach ($message_headers as $header_name => $header_value) { - $message .= $header_name.': '.$header_value.$this->line_break; - } - - // add message body - $message .= $this->line_break.$message_body; - - return $message; - } - - return false; - } - - /** - * Sets just happened error code - * - * @param string $code - * @param Array $params additional error params - * @return bool - */ - function SetError($code, $params = null, $fatal = true) - { - $error_msgs = Array ( - 'MAIL_NOT_FOUND' => 'the mail() function is not available in this PHP installation', - - 'MISSING_CONTENT_TYPE' => 'it was added a part without Content-Type: defined', - 'INVALID_CONTENT_TYPE' => 'Content-Type: %s not yet supported', - 'INVALID_MULTIPART_SUBTYPE' => 'multipart Content-Type sub_type %s not yet supported', - 'FILE_PART_OPEN_ERROR' => 'could not open part file %s', - 'FILE_PART_DATA_ERROR' => 'the length of the file that was read does not match the size of the part file %s due to possible data corruption', - 'FILE_PART_DATA_MISSING' => 'it was added a part without a body PART', - 'INVALID_ENCODING' => '%s is not yet a supported encoding type', - - 'MISSING_FILE_DATA' => 'file part data is missing', - 'MISSING_FILE_NAME' => 'it is not possible to determine content type from the name', - 'INVALID_DISPOSITION' => '%s is not a supported message part content disposition', - - 'MESSAGE_TEXT_MISSING' => 'text part of message was not defined', - 'MESSAGE_COMPOSE_ERROR' => 'unknown message composing error', - - 'HEADER_MISSING' => 'header %s is required', - - // SMTP errors - 'INVALID_COMMAND' => 'Commands cannot contain newlines', - 'CONNECTION_TERMINATED' => 'Connection was unexpectedly closed', - 'HELO_ERROR' => 'HELO was not accepted: %s', - 'AUTH_METHOD_NOT_SUPPORTED' => '%s is not a supported authentication method', - 'AUTH_METHOD_NOT_IMPLEMENTED' => '%s is not a implemented authentication method', - ); - - if (!is_array($params)) { - $params = Array (); - } - - trigger_error('mail error: '.vsprintf($error_msgs[$code], $params), $fatal ? E_USER_ERROR : E_USER_WARNING); - - return false; - } - - /** - * Simple method of message sending - * - * @param string $from_email - * @param string $to_email - * @param string $subject - * @param string $from_name - * @param string $to_name - */ - function Send($from_email, $to_email, $subject, $from_name = '', $to_name = '') - { - $this->SetSubject($subject); - $this->SetFrom($from_email, trim($from_name) ? trim($from_name) : $from_email); - - if (!isset($this->headers['Return-Path'])) { - $this->SetReturnPath($from_email); - } - - $this->SetEncodedEmailHeader('To', $to_email, $to_name ? $to_name : $to_email); - - return $this->Deliver(); - } - - /** - * Prepares class for sending another message - * - */ - function Clear() - { - $this->headers = Array (); - $this->bodyPartNumber = false; - $this->parts = Array(); - $this->guessOptions = Array ( - 'attachments' => Array(), - 'inline_attachments' => Array (), - 'text_part' => false, - 'html_part' => false, - ); - - $this->SetCharset(null, true); - } - - /** - * Sends message via php mail function - * - * @param Array $message_headers - * @param string $body - * - * @return bool - */ - function SendMail($message_headers, &$body) - { - if (!function_exists('mail')) { - return $this->SetError('MAIL_NOT_FOUND'); - } - - $to = $message_headers['To']; - $subject = $message_headers['Subject']; - $return_path = $message_headers['Return-Path']; - unset($message_headers['To'], $message_headers['Subject']); - - $headers = ''; - $header_separator = $this->Application->ConfigValue('MailFunctionHeaderSeparator') == 1 ? "\n" : "\r\n"; - foreach ($message_headers as $header_name => $header_value) { - $headers .= $header_name.': '.$header_value.$header_separator; - } - - if ($return_path) { - if (constOn('SAFE_MODE') || (defined('PHP_OS') && substr(PHP_OS, 0, 3) == 'WIN')) { - // safe mode restriction OR is windows - $return_path = ''; - } - } - - return mail($to, $subject, $body, $headers, $return_path ? '-f'.$return_path : null); - } - - /** - * Sends message via SMTP server - * - * @param Array $message_headers - * @param string $body - * - * @return bool - */ - function SendSMTP($message_headers, &$message_body) - { - if (!$this->SmtpConnect()) { - return false; - } - - $from = $this->ExtractRecipientEmail($message_headers['From']); - if (!$this->SmtpSetFrom($from)) { - return false; - } - - $recipients = ''; - $recipient_headers = Array ('To', 'Cc', 'Bcc'); - foreach ($recipient_headers as $recipient_header) { - if (isset($message_headers[$recipient_header])) { - $recipients .= ' '.$message_headers[$recipient_header]; - } - } - - $recipients_accepted = 0; - $recipients = $this->ExtractRecipientEmail($recipients, true); - foreach ($recipients as $recipient) { - if ($this->SmtpAddTo($recipient)) { - $recipients_accepted++; - } - } - - if ($recipients_accepted == 0) { - // none of recipients were accepted - return false; - } - - $headers = ''; - foreach ($message_headers as $header_name => $header_value) { - $headers .= $header_name.': '.$header_value.$this->line_break; - } - - if (!$this->SmtpSendMessage($headers . "\r\n" . $message_body)) { - return false; - } - - $this->SmtpDisconnect(); - - return true; - } - - /** - * Send a command to the server with an optional string of - * arguments. A carriage return / linefeed (CRLF) sequence will - * be appended to each command string before it is sent to the - * SMTP server. - * - * @param string $command The SMTP command to send to the server. - * @param string $args A string of optional arguments to append to the command. - * - * @return bool - * - */ - function SmtpSendCommand($command, $args = '') - { - if (!empty($args)) { - $command .= ' ' . $args; - } - - if (strcspn($command, "\r\n") !== strlen($command)) { - return $this->SetError('INVALID_COMMAND'); - } - - return $this->smtpSocket->write($command . "\r\n") === false ? false : true; - } - - /** - * Read a reply from the SMTP server. The reply consists of a response code and a response message. - * - * @param mixed $valid The set of valid response codes. These may be specified as an array of integer values or as a single integer value. - * - * @return bool - * - */ - function SmtpParseResponse($valid) - { - $this->smtpResponceCode = -1; - $this->smtpRespoceArguments = array(); - - while ($line = $this->smtpSocket->readLine()) { - // If we receive an empty line, the connection has been closed. - if (empty($line)) { - $this->SmtpDisconnect(); - return $this->SetError('CONNECTION_TERMINATED', null, false); - } - - // Read the code and store the rest in the arguments array. - $code = substr($line, 0, 3); - $this->smtpRespoceArguments[] = trim(substr($line, 4)); - - // Check the syntax of the response code. - if (is_numeric($code)) { - $this->smtpResponceCode = (int)$code; - } else { - $this->smtpResponceCode = -1; - break; - } - - // If this is not a multiline response, we're done. - if (substr($line, 3, 1) != '-') { - break; - } - } - - // Compare the server's response code with the valid code. - if (is_int($valid) && ($this->smtpResponceCode === $valid)) { - return true; - } - - // If we were given an array of valid response codes, check each one. - if (is_array($valid)) { - foreach ($valid as $valid_code) { - if ($this->smtpResponceCode === $valid_code) { - return true; - } - } - } - - return false; - } - - /** - * Attempt to connect to the SMTP server. - * - * @param int $timeout The timeout value (in seconds) for the socket connection. - * @param bool $persistent Should a persistent socket connection be used ? - * - * @return bool - * - */ - function SmtpConnect($timeout = null, $persistent = false) - { - $result = $this->smtpSocket->connect($this->smtpParams['server'], $this->smtpParams['port'], $persistent, $timeout); - if (!$result) { - return false; - } - - if ($this->SmtpParseResponse(220) === false) { - return false; - } - elseif ($this->SmtpNegotiate() === false) { - return false; - } - - if ($this->smtpParams['use_auth']) { - $result = $this->SmtpAuthentificate($this->smtpParams['username'], $this->smtpParams['password']); - if (!$result) { - // authentification failed - return false; - } - } - - return true; - } - - /** - * Attempt to disconnect from the SMTP server. - * - * @return bool - */ - function SmtpDisconnect() - { - if ($this->SmtpSendCommand('QUIT') === false) { - return false; - } - elseif ($this->SmtpParseResponse(221) === false) { - return false; - } - - return $this->smtpSocket->disconnect(); - } - - /** - * Attempt to send the EHLO command and obtain a list of ESMTP - * extensions available, and failing that just send HELO. - * - * @return bool - */ - function SmtpNegotiate() - { - if (!$this->SmtpSendCommand('EHLO', $this->smtpParams['localhost'])) { - return false; - } - - if (!$this->SmtpParseResponse(250)) { - // If we receive a 503 response, we're already authenticated. - if ($this->smtpResponceCode === 503) { - return true; - } - - // If the EHLO failed, try the simpler HELO command. - if (!$this->SmtpSendCommand('HELO', $this->smtpParams['localhost'])) { - return false; - } - - if (!$this->SmtpParseResponse(250)) { - return $this->SetError('HELO_ERROR', Array($this->smtpResponceCode), false); - } - - return true; - } - - foreach ($this->smtpRespoceArguments as $argument) { - $verb = strtok($argument, ' '); - $arguments = substr($argument, strlen($verb) + 1, strlen($argument) - strlen($verb) - 1); - $this->smtpFeatures[$verb] = $arguments; - } - - return true; - } - - /** - * Attempt to do SMTP authentication. - * - * @param string The userid to authenticate as. - * @param string The password to authenticate with. - * @param string The requested authentication method. If none is specified, the best supported method will be used. - * - * @return bool - */ - function SmtpAuthentificate($uid, $pwd , $method = '') - { - if (empty($this->smtpFeatures['AUTH'])) { - // server doesn't understand AUTH command, then don't authentificate - return true; - } - - $available_methods = explode(' ', $this->smtpFeatures['AUTH']); // methods supported by SMTP server - - if (empty($method)) { - foreach ($this->smtpAuthMethods as $supported_method) { - // check if server supports methods, that we have implemented - if (in_array($supported_method, $available_methods)) { - $method = $supported_method; - break; - } - } - } else { - $method = strtoupper($method); - } - - if (!in_array($method, $available_methods)) { - // coosen method is not supported by server - return $this->SetError('AUTH_METHOD_NOT_SUPPORTED', Array($method)); - } - - switch ($method) { - case 'CRAM-MD5': - $result = $this->_authCRAM_MD5($uid, $pwd); - break; - - case 'LOGIN': - $result = $this->_authLogin($uid, $pwd); - break; - case 'PLAIN': - $result = $this->_authPlain($uid, $pwd); - break; - default: - return $this->SetError('AUTH_METHOD_NOT_IMPLEMENTED', Array($method)); - break; - } - - return $result; - } - - /** - * Function which implements HMAC MD5 digest - * - * @param string $key The secret key - * @param string $data The data to protect - * @return string The HMAC MD5 digest - */ - function _HMAC_MD5($key, $data) - { - if (strlen($key) > 64) { - $key = pack('H32', md5($key)); - } - - if (strlen($key) < 64) { - $key = str_pad($key, 64, chr(0)); - } - - $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64); - $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64); - - $inner = pack('H32', md5($k_ipad . $data)); - $digest = md5($k_opad . $inner); - - return $digest; - } - - /** - * Authenticates the user using the CRAM-MD5 method. - * - * @param string The userid to authenticate as. - * @param string The password to authenticate with. - * - * @return bool - */ - function _authCRAM_MD5($uid, $pwd) - { - if (!$this->SmtpSendCommand('AUTH', 'CRAM-MD5')) { - return false; - } - - // 334: Continue authentication request - if (!$this->SmtpParseResponse(334)) { - // 503: Error: already authenticated - return $this->smtpResponceCode === 503 ? true : false; - } - - $challenge = base64_decode($this->smtpRespoceArguments[0]); - $auth_str = base64_encode($uid . ' ' . $this->_HMAC_MD5($pwd, $challenge)); - - if (!$this->SmtpSendCommand($auth_str)) { - return false; - } - - // 235: Authentication successful - if (!$this->SmtpParseResponse(235)) { - return false; - } - - return true; - } - - /** - * Authenticates the user using the LOGIN method. - * - * @param string The userid to authenticate as. - * @param string The password to authenticate with. - * - * @return bool - */ - function _authLogin($uid, $pwd) - { - if (!$this->SmtpSendCommand('AUTH', 'LOGIN')) { - return false; - } - - // 334: Continue authentication request - if (!$this->SmtpParseResponse(334)) { - // 503: Error: already authenticated - return $this->smtpResponceCode === 503 ? true : false; - } - - if (!$this->SmtpSendCommand(base64_encode($uid))) { - return false; - } - - // 334: Continue authentication request - if (!$this->SmtpParseResponse(334)) { - return false; - } - - if (!$this->SmtpSendCommand(base64_encode($pwd))) { - return false; - } - - // 235: Authentication successful - if (!$this->SmtpParseResponse(235)) { - return false; - } - - return true; - } - - /** - * Authenticates the user using the PLAIN method. - * - * @param string The userid to authenticate as. - * @param string The password to authenticate with. - * - * @return bool - */ - function _authPlain($uid, $pwd) - { - if (!$this->SmtpSendCommand('AUTH', 'PLAIN')) { - return false; - } - - // 334: Continue authentication request - if (!$this->SmtpParseResponse(334)) { - // 503: Error: already authenticated - return $this->smtpResponceCode === 503 ? true : false; - } - - $auth_str = base64_encode(chr(0) . $uid . chr(0) . $pwd); - - if (!$this->SmtpSendCommand($auth_str)) { - return false; - } - - // 235: Authentication successful - if (!$this->SmtpParseResponse(235)) { - return false; - } - - return true; - } - - /** - * Send the MAIL FROM: command. - * - * @param string $sender The sender (reverse path) to set. - * @param string $params String containing additional MAIL parameters, such as the NOTIFY flags defined by RFC 1891 or the VERP protocol. - * - * @return bool - */ - function SmtpSetFrom($sender, $params = null) - { - $args = "FROM:<$sender>"; - if (is_string($params)) { - $args .= ' ' . $params; - } - - if (!$this->SmtpSendCommand('MAIL', $args)) { - return false; - } - if (!$this->SmtpParseResponse(250)) { - return false; - } - - return true; - } - - /** - * Send the RCPT TO: command. - * - * @param string $recipient The recipient (forward path) to add. - * @param string $params String containing additional RCPT parameters, such as the NOTIFY flags defined by RFC 1891. - * - * @return bool - */ - function SmtpAddTo($recipient, $params = null) - { - $args = "TO:<$recipient>"; - if (is_string($params)) { - $args .= ' ' . $params; - } - - if (!$this->SmtpSendCommand('RCPT', $args)) { - return false; - } - - if (!$this->SmtpParseResponse(array(250, 251))) { - return false; - } - - return true; - } - - /** - * Send the DATA command. - * - * @param string $data The message body to send. - * - * @return bool - */ - function SmtpSendMessage($data) - { - /* RFC 1870, section 3, subsection 3 states "a value of zero - * indicates that no fixed maximum message size is in force". - * Furthermore, it says that if "the parameter is omitted no - * information is conveyed about the server's fixed maximum - * message size". */ - if (isset($this->smtpFeatures['SIZE']) && ($this->smtpFeatures['SIZE'] > 0)) { - if (strlen($data) >= $this->smtpFeatures['SIZE']) { - $this->SmtpDisconnect(); - return $this->SetError('Message size excedes the server limit', null, false); - } - } - - // Quote the data based on the SMTP standards - - // Change Unix (\n) and Mac (\r) linefeeds into Internet-standard CRLF (\r\n) linefeeds. - $data = preg_replace(Array('/(?SmtpSendCommand('DATA')) { - return false; - } - if (!$this->SmtpParseResponse(354)) { - return false; - } - - if ($this->smtpSocket->write($data . "\r\n.\r\n") === false) { - return false; - } - if (!$this->SmtpParseResponse(250)) { - return false; - } - - return true; - } - - /** - * Sets global charset for every message part - * - * @param string $charset - * @param bool set charset to default for current langauge - */ - function SetCharset($charset, $is_system = false) - { - if ($is_system) { - $language =& $this->Application->recallObject('lang.current'); - /* @var $language LanguagesItem */ - - $charset = $language->GetDBField('Charset') ? $language->GetDBField('Charset') : 'ISO-8859-1'; - } - - $this->charset = $charset; - } - - /** - * Allows to extract recipient's name from text by specifying it's email - * - * @param string $text - * @param string $email - * @return string - */ - function ExtractRecipientName($text, $email = '') - { - $lastspace = strrpos($text, ' '); - $name = trim(substr($text, 0, $lastspace - strlen($text)), " \r\n\t\0\x0b\"'"); - if (empty($name)) { - $name = $email; - } - return $name; - } - - /** - * Takes $text and returns an email address from it - * Set $multiple to true to retrieve all found addresses - * Returns false if no addresses were found - * - * @param string $text - * @param bool $multiple - * @param bool $allowOnlyDomain - * @return string - */ - function ExtractRecipientEmail($text, $multiple = false, $allowOnlyDomain = false) { - if ($allowOnlyDomain) { - $pattern = '/(('.REGEX_EMAIL_USER.'@)?'.REGEX_EMAIL_DOMAIN.')/i'; - } else { - $pattern = '/('.REGEX_EMAIL_USER.'@'.REGEX_EMAIL_DOMAIN.')/i'; - } - if ($multiple) { - if (preg_match_all($pattern, $text, $findemail) >= 1) { - return $findemail[1]; - } else { - return false; - } - } else { - if (preg_match($pattern, $text, $findemail) == 1) { - return $findemail[1]; - } else { - return false; - } - } - } - - /** - * Returns array of recipient names and emails - * - * @param string $list - * @param string $separator - * @return Array - */ - function GetRecipients($list, $separator = ';') - { - // by MIME specs recipients should be separated using "," symbol, - // but users can write ";" too (like in OutLook) - - if (!trim($list)) { - return false; - } - - $list = explode(',', str_replace($separator, ',', $list)); - - $ret = Array (); - foreach ($list as $recipient) { - $email = $this->ExtractRecipientEmail($recipient); - if (!$email) { - // invalid email format -> error - return false; - } - $name = $this->ExtractRecipientName($recipient, $email); - $ret[] = Array('Name' => $name, 'Email' => $email); - } - - return $ret; - } - - /* methods for nice header setting */ - - /** - * Sets "From" header. - * - * @param string $email - * @param string $first_last_name FirstName and LastName or just FirstName - * @param string $last_name LastName (if not specified in previous parameter) - */ - function SetFrom($email, $first_last_name, $last_name = '') - { - $name = rtrim($first_last_name.' '.$last_name, ' '); - $this->SetEncodedEmailHeader('From', $email, $name ? $name : $email); - - if (!isset($this->headers['Return-Path'])) { - $this->SetReturnPath($email); - } - } - - /** - * Sets "Return-Path" header (useful for spammers) - * - * @param string $email - */ - function SetReturnPath($email) - { - $this->SetHeader('Return-Path', $email); - } - - /** - * Adds one more recipient into "To" header - * - * @param string $email - * @param string $first_last_name FirstName and LastName or just FirstName - * @param string $last_name LastName (if not specified in previous parameter) - */ - function AddTo($email, $first_last_name = '', $last_name = '') - { - $name = rtrim($first_last_name.' '.$last_name, ' '); - $this->AddRecipient('To', $email, $name); - } - - /** - * Adds one more recipient into "Cc" header - * - * @param string $email - * @param string $first_last_name FirstName and LastName or just FirstName - * @param string $last_name LastName (if not specified in previous parameter) - */ - function AddCc($email, $first_last_name = '', $last_name = '') - { - $name = rtrim($first_last_name.' '.$last_name, ' '); - $this->AddRecipient('Cc', $email, $name); - } - - /** - * Adds one more recipient into "Bcc" header - * - * @param string $email - * @param string $first_last_name FirstName and LastName or just FirstName - * @param string $last_name LastName (if not specified in previous parameter) - */ - function AddBcc($email, $first_last_name = '', $last_name = '') - { - $name = rtrim($first_last_name.' '.$last_name, ' '); - $this->AddRecipient('Bcc', $email, $name); - } - - /** - * Adds one more recipient to specified header - * - * @param string $header_name - * @param string $email - * @param string $name - */ - function AddRecipient($header_name, $email, $name = '') - { - if (!$name) { - $name = $email; - } - - $value = isset($this->headers[$header_name]) ? $this->headers[$header_name] : ''; - $value .= ', '.$this->QuotedPrintableEncode($name, $this->charset).' <'.$email.'>'; - - $value = preg_replace('/^,(.*)/', '\\1', $value); // remove first comma - $this->SetHeader($header_name, $value); - } - - /** - * Sets "Subject" header. - * - * @param string $subject message subject - */ - function SetSubject($subject) - { - $this->setEncodedHeader('Subject', $subject); - } - - /** - * Sets HTML part of message - * - * @param string $html - */ - function SetHTML($html) - { - $this->CreateTextHtmlPart($html, true); - } - - /** - * Sets Plain-Text part of message - * - * @param string $plain_text - */ - function SetPlain($plain_text) - { - $this->CreateTextHtmlPart($plain_text); - } - - /** - * Sets HTML and optionally plain part of the message - * - * @param string $html - * @param string $plain_text - */ - function SetBody($html, $plain_text = '') - { - $this->SetHTML($html); - if ($plain_text) { - $this->SetPlain($plain_text); - } - } - - /** - * Performs mail delivery (supports delayed delivery) - * - * @param string $mesasge message, if not given, then use composed one - * @param bool $immediate_send send message now - * @param bool $immediate_clear clear message parts after message is sent - * - */ - function Deliver($message = null, $immediate_send = true, $immediate_clear = true) - { - if (isset($message)) { - // if message is given directly, then use it - if (is_array($message)) { - $message_headers =& $message[0]; - $message_body =& $message[1]; - } - else { - $message_headers = Array (); - list ($headers, $message_body) = explode("\n\n", $message, 2); - $headers = explode("\n", $headers); - foreach ($headers as $header) { - $header = explode(':', $header, 2); - $message_headers[ trim($header[0]) ] = trim($header[1]); - } - } - $composed = true; - } else { - // direct message not given, then assemble message from available parts - $composed = $this->GetHeadersAndBody($message_headers, $message_body); - } - - if ($composed) { - if ($immediate_send) { - $send_method = 'Send'.$this->sendMethod; - $result = $this->$send_method($message_headers, $message_body); - - if ($immediate_clear) { - $this->Clear(); - } - return $result; - } - else { - $fields_hash = Array ( - 'ToEmail' => $message_headers['To'], - 'Subject' => $message_headers['Subject'], - 'Queued' => adodb_mktime(), - 'SendRetries' => 0, - 'LastSendRetry' => 0, - ); - $fields_hash['MessageHeaders'] = serialize($message_headers); - $fields_hash['MessageBody'] =& $message_body; - $this->Conn->doInsert($fields_hash, TABLE_PREFIX.'EmailQueue'); - - if ($immediate_clear) { - $this->Clear(); - } - } - } - - // if not immediate send, then send result is positive :) - return !$immediate_send ? true : false; - } - } - -?> \ No newline at end of file