From: Andrey Kutejko Date: Tue, 4 Feb 2014 19:39:31 +0000 (+0200) Subject: place code into subfolder X-Git-Url: https://git.andy128k.dev/?a=commitdiff_plain;h=50ef927fe12e5f1cddb7f3f4d247de2a8b09721e;p=ipf-mail.git place code into subfolder --- diff --git a/composer.json b/composer.json index 6d743c9..71bb097 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "phpunit/phpunit": "3.7.*" }, "autoload": { - "classmap" : ["."] + "classmap" : ["src/"] } } diff --git a/exception.php b/exception.php deleted file mode 100644 index ef36fc7..0000000 --- a/exception.php +++ /dev/null @@ -1,6 +0,0 @@ -_charset = $charset; - } - - public function getCharset() - { - return $this->_charset; - } - - public function setType($type) - { - $allowed = array( - IPF_Mime::MULTIPART_ALTERNATIVE, - IPF_Mime::MULTIPART_MIXED, - IPF_Mime::MULTIPART_RELATED, - ); - if (!in_array($type, $allowed)) { - throw new IPF_Mail_Exception('Invalid content type "' . $type . '"'); - } - - $this->_type = $type; - return $this; - } - - public function getType() - { - return $this->_type; - } - - public function setMimeBoundary($boundary) - { - $this->_mimeBoundary = $boundary; - - return $this; - } - - public function getMimeBoundary() - { - return $this->_mimeBoundary; - } - - public function setBodyText($txt, $charset = null, $encoding = IPF_Mime::ENCODING_QUOTEDPRINTABLE) - { - if ($charset === null) { - $charset = $this->_charset; - } - - $mp = new IPF_Mime_Part($txt); - $mp->encoding = $encoding; - $mp->type = IPF_Mime::TYPE_TEXT; - $mp->disposition = IPF_Mime::DISPOSITION_INLINE; - $mp->charset = $charset; - - $this->_bodyText = $mp; - - return $this; - } - - public function getBodyText($textOnly = false) - { - if ($textOnly && $this->_bodyText) { - $body = $this->_bodyText; - return $body->getContent(); - } - - return $this->_bodyText; - } - - public function setBodyHtml($html, $charset = null, $encoding = IPF_Mime::ENCODING_QUOTEDPRINTABLE) - { - if ($charset === null) { - $charset = $this->_charset; - } - - $mp = new IPF_Mime_Part($html); - $mp->encoding = $encoding; - $mp->type = IPF_Mime::TYPE_HTML; - $mp->disposition = IPF_Mime::DISPOSITION_INLINE; - $mp->charset = $charset; - - $this->_bodyHtml = $mp; - - return $this; - } - - public function getBodyHtml($htmlOnly = false) - { - if ($htmlOnly && $this->_bodyHtml) { - $body = $this->_bodyHtml; - return $body->getContent(); - } - - return $this->_bodyHtml; - } - - public function addAttachment(IPF_Mime_Part $attachment) - { - $this->addPart($attachment); - $this->hasAttachments = true; - - return $this; - } - - public function createAttachment($body, - $mimeType = IPF_Mime::TYPE_OCTETSTREAM, - $disposition = IPF_Mime::DISPOSITION_ATTACHMENT, - $encoding = IPF_Mime::ENCODING_BASE64, - $filename = null) - { - - $mp = new IPF_Mime_Part($body); - $mp->encoding = $encoding; - $mp->type = $mimeType; - $mp->disposition = $disposition; - $mp->filename = $filename; - - $this->addAttachment($mp); - - return $mp; - } - - public function getPartCount() - { - return count($this->_parts); - } - - protected function _encodeHeader($value) - { - if (IPF_Mime::isPrintable($value)) { - return $value; - } else { - $quotedValue = IPF_Mime::encodeQuotedPrintable($value); - $quotedValue = str_replace(array('?', ' '), array('=3F', '=20'), $quotedValue); - return '=?' . $this->_charset . '?Q?' . $quotedValue . '?='; - } - } - - protected function _storeHeader($headerName, $value, $append=false) - { -// ?? $value = strtr($value,"\r\n\t",'???'); - if (isset($this->_headers[$headerName])) { - $this->_headers[$headerName][] = $value; - } else { - $this->_headers[$headerName] = array($value); - } - - if ($append) { - $this->_headers[$headerName]['append'] = true; - } - - } - - protected function _addRecipient($email, $to = false) - { - // prevent duplicates - $this->_recipients[$email] = 1; - - if ($to) { - $this->_to[] = $email; - } - } - - protected function _addRecipientAndHeader($headerName, $name, $email) - { - $email = strtr($email,"\r\n\t",'???'); - $this->_addRecipient($email, ('To' == $headerName) ? true : false); - if ($name != '') { - $name = '"' . $this->_encodeHeader($name) . '" '; - } - - $this->_storeHeader($headerName, $name .'<'. $email . '>', true); - } - - public function addTo($email, $name='') - { - $this->_addRecipientAndHeader('To', $name, $email); - return $this; - } - - public function addCc($email, $name='') - { - $this->_addRecipientAndHeader('Cc', $name, $email); - return $this; - } - - public function addBcc($email) - { - $this->_addRecipientAndHeader('Bcc', '', $email); - return $this; - } - - public function getRecipients() - { - return array_keys($this->_recipients); - } - - public function setFrom($email, $name = '') - { - if ($this->_from === null) { - $email = strtr($email,"\r\n\t",'???'); - $this->_from = $email; - $this->_storeHeader('From', $this->_encodeHeader('"'.$name.'"').' <'.$email.'>', true); - } else { - throw new IPF_Mail_Exception('From Header set twice'); - } - return $this; - } - - public function getFrom() - { - return $this->_from; - } - - public function setReturnPath($email) - { - if ($this->_returnPath === null) { - $email = strtr($email,"\r\n\t",'???'); - $this->_returnPath = $email; - $this->_storeHeader('Return-Path', $email, false); - } else { - throw new IPF_Mail_Exception('Return-Path Header set twice'); - } - return $this; - } - - public function getReturnPath() - { - if (null !== $this->_returnPath) { - return $this->_returnPath; - } - - return $this->_from; - } - - public function setSubject($subject) - { - if ($this->_subject === null) { - $subject = strtr($subject,"\r\n\t", '???'); - if (IPF_Mime::isPrintable($subject)) - $this->_subject = $subject; - else - $this->_subject = IPF_Mime::encodeQ($subject); - $this->_storeHeader('Subject', $this->_subject); - } else { - throw new IPF_Mail_Exception('Subject set twice'); - } - return $this; - } - - public function getSubject() - { - return $this->_subject; - } - - public function setDate($date = null) - { - if ($this->_date === null) { - if ($date === null) { - $date = date('r'); - } else if (is_int($date)) { - $date = date('r', $date); - } else if (is_string($date)) { - $date = strtotime($date); - if ($date === false || $date < 0) { - throw new IPF_Mail_Exception('String representations of Date Header must be ' . - 'strtotime()-compatible'); - } - $date = date('r', $date); - } else { - throw new IPF_Mail_Exception(__METHOD__ . ' only accepts UNIX timestamps and strtotime()-compatible strings'); - } - $this->_date = $date; - $this->_storeHeader('Date', $date); - } else { - throw new IPF_Mail_Exception('Date Header set twice'); - } - return $this; - } - - public function getDate() - { - return $this->_date; - } - - public function addHeader($name, $value, $append = false) - { - if (in_array(strtolower($name), array('to', 'cc', 'bcc', 'from', 'subject', 'return-path', 'date'))) { - throw new IPF_Mail_Exception('Cannot set standard header from addHeader()'); - } - - $value = strtr($value,"\r\n\t",'???'); - $value = $this->_encodeHeader($value); - $this->_storeHeader($name, $value, $append); - - return $this; - } - - public function getHeaders() - { - return $this->_headers; - } - - public function send($transport = null) - { - if ($transport === null) { - if (! self::$_defaultTransport instanceof IPF_Mail_Transport_Abstract) { - $transport = new IPF_Mail_Transport_Sendmail(); - } else { - $transport = self::$_defaultTransport; - } - } - - if (is_null($this->_date)) { - $this->setDate(); - } - - $transport->send($this); - - return $this; - } - -} diff --git a/mail/exception.php b/mail/exception.php deleted file mode 100644 index 8ec3e7a..0000000 --- a/mail/exception.php +++ /dev/null @@ -1,6 +0,0 @@ -_flags = array_combine($params['flags'], $params['flags']); - } - - parent::__construct($params); - } - - public function getTopLines() - { - return $this->_topLines; - } - - public function hasFlag($flag) - { - return isset($this->_flags[$flag]); - } - - public function getFlags() - { - return $this->_flags; - } -} diff --git a/mail/part.php b/mail/part.php deleted file mode 100644 index f74df14..0000000 --- a/mail/part.php +++ /dev/null @@ -1,238 +0,0 @@ -_mail = $params['handler']; - $this->_messageNum = $params['id']; - } - - if (isset($params['raw'])) { - IPF_Mime_Decode::splitMessage($params['raw'], $this->_headers, $this->_content); - } else if (isset($params['headers'])) { - if (is_array($params['headers'])) { - $this->_headers = $params['headers']; - } else { - if (!empty($params['noToplines'])) { - IPF_Mime_Decode::splitMessage($params['headers'], $this->_headers, $null); - } else { - IPF_Mime_Decode::splitMessage($params['headers'], $this->_headers, $this->_topLines); - } - } - if (isset($params['content'])) { - $this->_content = $params['content']; - } - } - } - - public function isMultipart() - { - try { - return stripos($this->contentType, 'multipart/') === 0; - } catch(IPF_Mail_Exception $e) { - return false; - } - } - - - public function getContent() - { - if ($this->_content !== null) { - return $this->_content; - } - - if ($this->_mail) { - return $this->_mail->getRawContent($this->_messageNum); - } else { - throw new IPF_Mail_Exception('no content'); - } - } - - protected function _cacheContent() - { - // caching content if we can't fetch parts - if ($this->_content === null && $this->_mail) { - $this->_content = $this->_mail->getRawContent($this->_messageNum); - } - - if (!$this->isMultipart()) { - return; - } - - // split content in parts - $boundary = $this->getHeaderField('content-type', 'boundary'); - if (!$boundary) { - throw new IPF_Mail_Exception('no boundary found in content type to split message'); - } - $parts = IPF_Mime_Decode::splitMessageStruct($this->_content, $boundary); - $counter = 1; - foreach ($parts as $part) { - $this->_parts[$counter++] = new self(array('headers' => $part['header'], 'content' => $part['body'])); - } - } - - public function getPart($num) - { - if (isset($this->_parts[$num])) { - return $this->_parts[$num]; - } - - if (!$this->_mail && $this->_content === null) { - throw new IPF_Mail_Exception('part not found'); - } - - if ($this->_mail && $this->_mail->hasFetchPart) { - // TODO: fetch part - // return - } - - $this->_cacheContent(); - - if (!isset($this->_parts[$num])) { - throw new IPF_Mail_Exception('part not found'); - } - - return $this->_parts[$num]; - } - - public function countParts() - { - if ($this->_countParts) { - return $this->_countParts; - } - - $this->_countParts = count($this->_parts); - if ($this->_countParts) { - return $this->_countParts; - } - - if ($this->_mail && $this->_mail->hasFetchPart) { - // TODO: fetch part - // return - } - - $this->_cacheContent(); - - $this->_countParts = count($this->_parts); - return $this->_countParts; - } - - public function getHeaders() - { - if ($this->_headers === null) { - if (!$this->_mail) { - $this->_headers = array(); - } else { - $part = $this->_mail->getRawHeader($this->_messageNum); - IPF_Mime_Decode::splitMessage($part, $this->_headers, $null); - } - } - - return $this->_headers; - } - - public function getHeader($name, $format = null) - { - if ($this->_headers === null) { - $this->getHeaders(); - } - - $lowerName = strtolower($name); - - if (!isset($this->_headers[$lowerName])) { - $lowerName = strtolower(preg_replace('%([a-z])([A-Z])%', '\1-\2', $name)); - if (!isset($this->_headers[$lowerName])) { - throw new IPF_Mail_Exception("no Header with Name $name found"); - } - } - $name = $lowerName; - - $header = $this->_headers[$name]; - - switch ($format) { - case 'string': - if (is_array($header)) { - $header = implode(IPF_Mime::LINEEND, $header); - } - break; - case 'array': - $header = (array)$header; - default: - // do nothing - } - - return $header; - } - - public function getHeaderField($name, $wantedPart = 0, $firstName = 0) { - return IPF_Mime_Decode::splitHeaderField(current($this->getHeader($name, 'array')), $wantedPart, $firstName); - } - - public function __get($name) - { - return $this->getHeader($name, 'string'); - } - - public function __toString() - { - return $this->getContent(); - } - - public function hasChildren() - { - $current = $this->current(); - return $current && $current instanceof IPF_Mail_Part && $current->isMultipart(); - } - - public function getChildren() - { - return $this->current(); - } - - public function valid() - { - if ($this->_countParts === null) { - $this->countParts(); - } - return $this->_iterationPos && $this->_iterationPos <= $this->_countParts; - } - - public function next() - { - ++$this->_iterationPos; - } - - public function key() - { - return $this->_iterationPos; - } - - public function current() - { - return $this->getPart($this->_iterationPos); - } - - public function rewind() - { - $this->countParts(); - $this->_iterationPos = 1; - } -} diff --git a/mail/protocol/abstract.php b/mail/protocol/abstract.php deleted file mode 100644 index 88eb17a..0000000 --- a/mail/protocol/abstract.php +++ /dev/null @@ -1,147 +0,0 @@ -_host = $host; - $this->_port = $port; - } - - public function __destruct() - { - $this->_disconnect(); - } - - abstract public function connect(); - public function getRequest() - { - return $this->_request; - } - - public function getResponse() - { - return $this->_response; - } - - public function getLog() - { - return $this->_log; - } - - public function resetLog() - { - $this->_log = ''; - } - - protected function _connect($remote) - { - $errorNum = 0; - $errorStr = ''; - - // open connection - $this->_socket = stream_socket_client($remote, $errorNum, $errorStr, self::TIMEOUT_CONNECTION); - - if ($this->_socket === false) { - if ($errorNum == 0) { - $errorStr = 'Could not open socket'; - } - throw new IPF_Mail_Exception($errorStr); - } - - if (($result = stream_set_timeout($this->_socket, self::TIMEOUT_CONNECTION)) === false) { - throw new IPF_Mail_Exception('Could not set stream timeout'); - } - - return $result; - } - - protected function _disconnect() - { - if (is_resource($this->_socket)) { - fclose($this->_socket); - } - } - - protected function _send($request) - { - if (!is_resource($this->_socket)) { - throw new IPF_Mail_Exception('No connection has been established to ' . $this->_host); - } - - $this->_request = $request; - - $result = fwrite($this->_socket, $request . self::EOL); - - // Save request to internal log - $this->_log .= $request . self::EOL; - - if ($result === false) { - throw new IPF_Mail_Exception('Could not send request to ' . $this->_host); - } - - return $result; - } - - protected function _receive($timeout = null) - { - if (!is_resource($this->_socket)) { - throw new IPF_Mail_Exception('No connection has been established to ' . $this->_host); - } - - // Adapters may wish to supply per-commend timeouts according to appropriate RFC - if ($timeout !== null) { - stream_set_timeout($this->_socket, $timeout); - } - - // Retrieve response - $reponse = fgets($this->_socket, 1024); - - // Save request to internal log - $this->_log .= $reponse; - - // Check meta data to ensure connection is still valid - $info = stream_get_meta_data($this->_socket); - - if (!empty($info['timed_out'])) { - throw new IPF_Mail_Exception($this->_host . ' has timed out'); - } - - if ($reponse === false) { - throw new IPF_Mail_Exception('Could not read from ' . $this->_host); - } - - return $reponse; - } - - protected function _expect($code, $timeout = null) - { - $this->_response = array(); - $cmd = ''; - $msg = ''; - if (!is_array($code)) { - $code = array($code); - } - do { - $this->_response[] = $result = $this->_receive($timeout); - sscanf($result, $this->_template, $cmd, $msg); - - if ($cmd === null || !in_array($cmd, $code)) { - throw new IPF_Mail_Exception($result); - } - - } while (strpos($msg, '-') === 0); // The '-' message prefix indicates an information string instead of a response string. - return $msg; - } -} diff --git a/mail/protocol/imap.php b/mail/protocol/imap.php deleted file mode 100644 index 2e05564..0000000 --- a/mail/protocol/imap.php +++ /dev/null @@ -1,505 +0,0 @@ -connect($host, $port, $ssl); - } - } - - public function __destruct() - { - $this->logout(); - } - - public function connect($host, $port = null, $ssl = false) - { - if ($ssl == 'SSL') { - $host = 'ssl://' . $host; - } - - if ($port === null) { - $port = $ssl === 'SSL' ? 993 : 143; - } - - $this->_socket = @fsockopen($host, $port); - if (!$this->_socket) { - throw new IPF_Mail_Exception('cannot connect to host'); - } - - if (!$this->_assumedNextLine('* OK')) { - throw new IPF_Mail_Exception('host doesn\'t allow connection'); - } - - if ($ssl === 'TLS') { - $result = $this->requestAndResponse('STARTTLS'); - $result = $result && stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); - if (!$result) { - throw new IPF_Mail_Exception('cannot enable TLS'); - } - } - } - - protected function _nextLine() - { - $line = @fgets($this->_socket); - if ($line === false) { - throw new IPF_Mail_Exception('cannot read - connection closed?'); - } - - return $line; - } - - protected function _assumedNextLine($start) - { - $line = $this->_nextLine(); - return strpos($line, $start) === 0; - } - - protected function _nextTaggedLine(&$tag) - { - $line = $this->_nextLine(); - - // seperate tag from line - list($tag, $line) = explode(' ', $line, 2); - - return $line; - } - - protected function _decodeLine($line) - { - $tokens = array(); - $stack = array(); - - /* - We start to decode the response here. The unterstood tokens are: - literal - "literal" or also "lit\\er\"al" - {bytes}literal - (literals*) - All tokens are returned in an array. Literals in braces (the last unterstood - token in the list) are returned as an array of tokens. I.e. the following response: - "foo" baz {3}bar ("f\\\"oo" bar) - would be returned as: - array('foo', 'baz', 'bar', array('f\\\"oo', 'bar')); - - // TODO: add handling of '[' and ']' to parser for easier handling of response text - */ - // replace any trailling including spaces with a single space - $line = rtrim($line) . ' '; - while (($pos = strpos($line, ' ')) !== false) { - $token = substr($line, 0, $pos); - while ($token[0] == '(') { - array_push($stack, $tokens); - $tokens = array(); - $token = substr($token, 1); - } - if ($token[0] == '"') { - if (preg_match('%^"((.|\\\\|\\")*?)" *%', $line, $matches)) { - $tokens[] = $matches[1]; - $line = substr($line, strlen($matches[0])); - continue; - } - } - if ($token[0] == '{') { - $endPos = strpos($token, '}'); - $chars = substr($token, 1, $endPos - 1); - if (is_numeric($chars)) { - $token = ''; - while (strlen($token) < $chars) { - $token .= $this->_nextLine(); - } - $line = ''; - if (strlen($token) > $chars) { - $line = substr($token, $chars); - $token = substr($token, 0, $chars); - } else { - $line .= $this->_nextLine(); - } - $tokens[] = $token; - $line = trim($line) . ' '; - continue; - } - } - if ($stack && $token[strlen($token) - 1] == ')') { - // closing braces are not seperated by spaces, so we need to count them - $braces = strlen($token); - $token = rtrim($token, ')'); - // only count braces if more than one - $braces -= strlen($token) + 1; - // only add if token had more than just closing braces - if ($token) { - $tokens[] = $token; - } - $token = $tokens; - $tokens = array_pop($stack); - // special handline if more than one closing brace - while ($braces-- > 0) { - $tokens[] = $token; - $token = $tokens; - $tokens = array_pop($stack); - } - } - $tokens[] = $token; - $line = substr($line, $pos + 1); - } - - // maybe the server forgot to send some closing braces - while ($stack) { - $child = $tokens; - $tokens = array_pop($stack); - $tokens[] = $child; - } - - return $tokens; - } - - public function readLine(&$tokens = array(), $wantedTag = '*', $dontParse = false) - { - $line = $this->_nextTaggedLine($tag); - if (!$dontParse) { - $tokens = $this->_decodeLine($line); - } else { - $tokens = $line; - } - - // if tag is wanted tag we might be at the end of a multiline response - return $tag == $wantedTag; - } - - public function readResponse($tag, $dontParse = false) - { - $lines = array(); - while (!$this->readLine($tokens, $tag, $dontParse)) { - $lines[] = $tokens; - } - - if ($dontParse) { - // last to chars are still needed for response code - $tokens = array(substr($tokens, 0, 2)); - } - // last line has response code - if ($tokens[0] == 'OK') { - return $lines ? $lines : true; - } else if ($tokens[0] == 'NO'){ - return false; - } - return null; - } - - public function sendRequest($command, $tokens = array(), &$tag = null) - { - if (!$tag) { - ++$this->_tagCount; - $tag = 'TAG' . $this->_tagCount; - } - - $line = $tag . ' ' . $command; - - foreach ($tokens as $token) { - if (is_array($token)) { - if (@fputs($this->_socket, $line . ' ' . $token[0] . "\r\n") === false) { - throw new IPF_Mail_Exception('cannot write - connection closed?'); - } - if (!$this->_assumedNextLine('+ ')) { - throw new IPF_Mail_Exception('cannot send literal string'); - } - $line = $token[1]; - } else { - $line .= ' ' . $token; - } - } - - if (@fputs($this->_socket, $line . "\r\n") === false) { - throw new IPF_Mail_Exception('cannot write - connection closed?'); - } - } - - public function requestAndResponse($command, $tokens = array(), $dontParse = false) - { - $this->sendRequest($command, $tokens, $tag); - $response = $this->readResponse($tag, $dontParse); - - return $response; - } - - public function escapeString($string) - { - if (func_num_args() < 2) { - if (strpos($string, "\n") !== false) { - return array('{' . strlen($string) . '}', $string); - } else { - return '"' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $string) . '"'; - } - } - $result = array(); - foreach (func_get_args() as $string) { - $result[] = $this->escapeString($string); - } - return $result; - } - - public function escapeList($list) - { - $result = array(); - foreach ($list as $v) { - if (is_array($v)) { - $result[] = $this->escapeList($v); - } else { -// $result[] = $this->escapeString($v); - $result[] = $v; - } - } - return '(' . implode(' ', $result) . ')'; - } - - public function login($user, $password) - { - return $this->requestAndResponse('LOGIN', $this->escapeString($user, $password), true); - } - - public function logout() - { - $result = false; - if ($this->_socket) { - try { - $result = $this->requestAndResponse('LOGOUT', array(), true); - } catch (IPF_Mail_Exception $e) { - // ignoring exception - } - fclose($this->_socket); - $this->_socket = null; - } - return $result; - } - - public function capability() - { - $response = $this->requestAndResponse('CAPABILITY'); - - if (!$response) { - return $response; - } - - $capabilities = array(); - foreach ($response as $line) { - $capabilities = array_merge($capabilities, $line); - } - return $capabilities; - } - - public function examineOrSelect($command = 'EXAMINE', $box = 'INBOX') - { - $this->sendRequest($command, array($this->escapeString($box)), $tag); - - $result = array(); - while (!$this->readLine($tokens, $tag)) { - if ($tokens[0] == 'FLAGS') { - array_shift($tokens); - $result['flags'] = $tokens; - continue; - } - switch ($tokens[1]) { - case 'EXISTS': - case 'RECENT': - $result[strtolower($tokens[1])] = $tokens[0]; - break; - case '[UIDVALIDITY': - $result['uidvalidity'] = (int)$tokens[2]; - break; - default: - // ignore - } - } - - if ($tokens[0] != 'OK') { - return false; - } - return $result; - } - - public function select($box = 'INBOX') - { - return $this->examineOrSelect('SELECT', $box); - } - - public function examine($box = 'INBOX') - { - return $this->examineOrSelect('EXAMINE', $box); - } - - public function fetch($items, $from, $to = null) - { - if (is_array($from)) { - $set = implode(',', $from); - } else if ($to === null) { - $set = (int)$from; - } else if ($to === INF) { - $set = (int)$from . ':*'; - } else { - $set = (int)$from . ':' . (int)$to; - } - - $items = (array)$items; - $itemList = $this->escapeList($items); - - $this->sendRequest('FETCH', array($set, $itemList), $tag); - - $result = array(); - while (!$this->readLine($tokens, $tag)) { - // ignore other responses - if ($tokens[1] != 'FETCH') { - continue; - } - // ignore other messages - if ($to === null && !is_array($from) && $tokens[0] != $from) { - continue; - } - // if we only want one item we return that one directly - if (count($items) == 1) { - if ($tokens[2][0] == $items[0]) { - $data = $tokens[2][1]; - } else { - // maybe the server send an other field we didn't wanted - $count = count($tokens[2]); - // we start with 2, because 0 was already checked - for ($i = 2; $i < $count; $i += 2) { - if ($tokens[2][$i] != $items[0]) { - continue; - } - $data = $tokens[2][$i + 1]; - break; - } - } - } else { - $data = array(); - while (key($tokens[2]) !== null) { - $data[current($tokens[2])] = next($tokens[2]); - next($tokens[2]); - } - } - // if we want only one message we can ignore everything else and just return - if ($to === null && !is_array($from) && $tokens[0] == $from) { - // we still need to read all lines - while (!$this->readLine($tokens, $tag)); - return $data; - } - $result[$tokens[0]] = $data; - } - - if ($to === null && !is_array($from)) { - throw new IPF_Mail_Exception('the single id was not found in response'); - } - - return $result; - } - - public function listMailbox($reference = '', $mailbox = '*') - { - $result = array(); - $list = $this->requestAndResponse('LIST', $this->escapeString($reference, $mailbox)); - if (!$list || $list === true) { - return $result; - } - - foreach ($list as $item) { - if (count($item) != 4 || $item[0] != 'LIST') { - continue; - } - $result[$item[3]] = array('delim' => $item[2], 'flags' => $item[1]); - } - - return $result; - } - - public function store(array $flags, $from, $to = null, $mode = null, $silent = true) - { - $item = 'FLAGS'; - if ($mode == '+' || $mode == '-') { - $item = $mode . $item; - } - if ($silent) { - $item .= '.SILENT'; - } - - $flags = $this->escapeList($flags); - $set = (int)$from; - if ($to != null) { - $set .= ':' . ($to == INF ? '*' : (int)$to); - } - - $result = $this->requestAndResponse('STORE', array($set, $item, $flags), $silent); - - if ($silent) { - return $result ? true : false; - } - - $tokens = $result; - $result = array(); - foreach ($tokens as $token) { - if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') { - continue; - } - $result[$token[0]] = $token[2][1]; - } - - return $result; - } - - public function append($folder, $message, $flags = null, $date = null) - { - $tokens = array(); - $tokens[] = $this->escapeString($folder); - if ($flags !== null) { - $tokens[] = $this->escapeList($flags); - } - if ($date !== null) { - $tokens[] = $this->escapeString($date); - } - $tokens[] = $this->escapeString($message); - - return $this->requestAndResponse('APPEND', $tokens, true); - } - - public function copy($folder, $from, $to = null) - { - $set = (int)$from; - if ($to != null) { - $set .= ':' . ($to == INF ? '*' : (int)$to); - } - - return $this->requestAndResponse('COPY', array($set, $this->escapeString($folder)), true); - } - - public function create($folder) - { - return $this->requestAndResponse('CREATE', array($this->escapeString($folder)), true); - } - - public function rename($old, $new) - { - return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true); - } - - public function delete($folder) - { - return $this->requestAndResponse('DELETE', array($this->escapeString($folder)), true); - } - - public function expunge() - { - // TODO: parse response? - return $this->requestAndResponse('EXPUNGE'); - } - - public function noop() - { - // TODO: parse response - return $this->requestAndResponse('NOOP'); - } -} diff --git a/mail/protocol/pop3.php b/mail/protocol/pop3.php deleted file mode 100644 index cd148c9..0000000 --- a/mail/protocol/pop3.php +++ /dev/null @@ -1,246 +0,0 @@ -connect($host, $port, $ssl); - } - } - - public function __destruct() - { - $this->logout(); - } - - public function connect($host, $port = null, $ssl = false) - { - if ($ssl == 'SSL') { - $host = 'ssl://' . $host; - } - - if ($port === null) { - $port = $ssl == 'SSL' ? 995 : 110; - } - - $this->_socket = @fsockopen($host, $port); - if (!$this->_socket) { - throw new IPF_Mail_Exception('cannot connect to host'); - } - - $welcome = $this->readResponse(); - - strtok($welcome, '<'); - $this->_timestamp = strtok('>'); - if (!strpos($this->_timestamp, '@')) { - $this->_timestamp = null; - } else { - $this->_timestamp = '<' . $this->_timestamp . '>'; - } - - if ($ssl === 'TLS') { - $this->request('STLS'); - $result = stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); - if (!$result) { - throw new IPF_Mail_Exception('cannot enable TLS'); - } - } - - return $welcome; - } - - public function sendRequest($request) - { - $result = @fputs($this->_socket, $request . "\r\n"); - if (!$result) { - throw new IPF_Mail_Exception('send failed - connection closed?'); - } - } - - public function readResponse($multiline = false) - { - $result = @fgets($this->_socket); - if (!is_string($result)) { - throw new IPF_Mail_Exception('read failed - connection closed?'); - } - - $result = trim($result); - if (strpos($result, ' ')) { - list($status, $message) = explode(' ', $result, 2); - } else { - $status = $result; - $message = ''; - } - - if ($status != '+OK') { - throw new IPF_Mail_Exception('last request failed'); - } - - if ($multiline) { - $message = ''; - $line = fgets($this->_socket); - while ($line && trim($line) != '.') { - $message .= $line; - $line = fgets($this->_socket); - }; - } - - return $message; - } - - public function request($request, $multiline = false) - { - $this->sendRequest($request); - return $this->readResponse($multiline); - } - - public function logout() - { - if (!$this->_socket) { - return; - } - - try { - $this->request('QUIT'); - } catch (IPF_Mail_Exception $e) { - // ignore error - we're closing the socket anyway - } - - fclose($this->_socket); - $this->_socket = null; - } - - public function capa() - { - $result = $this->request('CAPA', true); - return explode("\n", $result); - } - - public function login($user, $password, $tryApop = true) - { - if ($tryApop && $this->_timestamp) { - try { - $this->request("APOP $user " . md5($this->_timestamp . $password)); - return; - } catch (IPF_Mail_Exception $e) { - // ignore - } - } - - $result = $this->request("USER $user"); - $result = $this->request("PASS $password"); - } - - public function status(&$messages, &$octets) - { - $messages = 0; - $octets = 0; - $result = $this->request('STAT'); - - list($messages, $octets) = explode(' ', $result); - } - - public function getList($msgno = null) - { - if ($msgno !== null) { - $result = $this->request("LIST $msgno"); - - list(, $result) = explode(' ', $result); - return (int)$result; - } - - $result = $this->request('LIST', true); - $messages = array(); - $line = strtok($result, "\n"); - while ($line) { - list($no, $size) = explode(' ', trim($line)); - $messages[(int)$no] = (int)$size; - $line = strtok("\n"); - } - - return $messages; - } - - public function uniqueid($msgno = null) - { - if ($msgno !== null) { - $result = $this->request("UIDL $msgno"); - - list(, $result) = explode(' ', $result); - return $result; - } - - $result = $this->request('UIDL', true); - - $result = explode("\n", $result); - $messages = array(); - foreach ($result as $line) { - if (!$line) { - continue; - } - list($no, $id) = explode(' ', trim($line), 2); - $messages[(int)$no] = $id; - } - - return $messages; - - } - - public function top($msgno, $lines = 0, $fallback = false) - { - if ($this->hasTop === false) { - if ($fallback) { - return $this->retrieve($msgno); - } else { - throw new IPF_Mail_Exception('top not supported and no fallback wanted'); - } - } - $this->hasTop = true; - - $lines = (!$lines || $lines < 1) ? 0 : (int)$lines; - - try { - $result = $this->request("TOP $msgno $lines", true); - } catch (IPF_Mail_Exception $e) { - $this->hasTop = false; - if ($fallback) { - $result = $this->retrieve($msgno); - } else { - throw $e; - } - } - - return $result; - } - - public function retrive($msgno) - { - return $this->retrieve($msgno); - } - - public function retrieve($msgno) - { - $result = $this->request("RETR $msgno", true); - return $result; - } - - public function noop() - { - $this->request('NOOP'); - } - - public function delete($msgno) - { - $this->request("DELE $msgno"); - } - - public function undelete() - { - $this->request('RSET'); - } -} diff --git a/mail/protocol/smtp.php b/mail/protocol/smtp.php deleted file mode 100644 index 008f26f..0000000 --- a/mail/protocol/smtp.php +++ /dev/null @@ -1,199 +0,0 @@ -_secure = 'tls'; - break; - - case 'ssl': - $this->_transport = 'ssl'; - $this->_secure = 'ssl'; - if ($port == null) { - $port = 465; - } - break; - - default: - throw new IPF_Mail_Exception($config['ssl'] . ' is unsupported SSL type'); - break; - } - } - - // If no port has been specified then check the master PHP ini file. Defaults to 25 if the ini setting is null. - if ($port == null) { - if (($port = ini_get('smtp_port')) == '') { - $port = 25; - } - } - - parent::__construct($host, $port); - } - - - public function connect() - { - return $this->_connect($this->_transport . '://' . $this->_host . ':'. $this->_port); - } - - public function helo($host = '127.0.0.1') - { - // Respect RFC 2821 and disallow HELO attempts if session is already initiated. - if ($this->_sess === true) { - throw new IPF_Mail_Exception('Cannot issue HELO to existing session'); - } - - // Validate client hostname - if (!$this->_validHost->isValid($host)) { - throw new IPF_Mail_Exception(join(', ', $this->_validHost->getMessage())); - } - - // Initiate helo sequence - $this->_expect(220, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 - $this->_ehlo($host); - - // If a TLS session is required, commence negotiation - if ($this->_secure == 'tls') { - $this->_send('STARTTLS'); - $this->_expect(220, 180); - if (!stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { - throw new IPF_Mail_Exception('Unable to connect via TLS'); - } - $this->_ehlo($host); - } - - $this->_startSession(); - $this->auth(); - } - - protected function _ehlo($host) - { - // Support for older, less-compliant remote servers. Tries multiple attempts of EHLO or HELO. - try { - $this->_send('EHLO ' . $host); - $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 - } catch (IPF_Mail_Exception $e) { - $this->_send('HELO ' . $host); - $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 - } catch (IPF_Mail_Exception $e) { - throw $e; - } - } - - public function mail($from) - { - if ($this->_sess !== true) { - throw new IPF_Mail_Exception('A valid session has not been started'); - } - - $this->_send('MAIL FROM:<' . $from . '>'); - $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 - - // Set mail to true, clear recipients and any existing data flags as per 4.1.1.2 of RFC 2821 - $this->_mail = true; - $this->_rcpt = false; - $this->_data = false; - } - - public function rcpt($to) - { - if ($this->_mail !== true) { - throw new IPF_Mail_Exception('No sender reverse path has been supplied'); - } - - // Set rcpt to true, as per 4.1.1.3 of RFC 2821 - $this->_send('RCPT TO:<' . $to . '>'); - $this->_expect(array(250, 251), 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 - $this->_rcpt = true; - } - - public function data($data) - { - // Ensure recipients have been set - if ($this->_rcpt !== true) { - throw new IPF_Mail_Exception('No recipient forward path has been supplied'); - } - - $this->_send('DATA'); - $this->_expect(354, 120); // Timeout set for 2 minutes as per RFC 2821 4.5.3.2 - - foreach (explode(IPF_Mime::LINEEND, $data) as $line) { - if (strpos($line, '.') === 0) { - // Escape lines prefixed with a '.' - $line = '.' . $line; - } - $this->_send($line); - } - - $this->_send('.'); - $this->_expect(250, 600); // Timeout set for 10 minutes as per RFC 2821 4.5.3.2 - $this->_data = true; - } - - public function rset() - { - $this->_send('RSET'); - // MS ESMTP doesn't follow RFC, see [ZF-1377] - $this->_expect(array(250, 220)); - - $this->_mail = false; - $this->_rcpt = false; - $this->_data = false; - } - - public function noop() - { - $this->_send('NOOP'); - $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 - } - - public function vrfy($user) - { - $this->_send('VRFY ' . $user); - $this->_expect(array(250, 251, 252), 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 - } - - public function quit() - { - if ($this->_sess) { - $this->_send('QUIT'); - $this->_expect(221, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 - $this->_stopSession(); - } - } - - public function auth() - { - if ($this->_auth === true) { - throw new IPF_Mail_Exception('Already authenticated for this session'); - } - } - - public function disconnect() - { - $this->_disconnect(); - } - - protected function _startSession() - { - $this->_sess = true; - } - - protected function _stopSession() - { - $this->_sess = false; - } -} diff --git a/mail/protocol/smtp/auth/crammd5.php b/mail/protocol/smtp/auth/crammd5.php deleted file mode 100644 index 576033e..0000000 --- a/mail/protocol/smtp/auth/crammd5.php +++ /dev/null @@ -1,49 +0,0 @@ -_username = $config['username']; - } - if (isset($config['password'])) { - $this->_password = $config['password']; - } - } - - parent::__construct($host, $port, $config); - } - - public function auth() - { - // Ensure AUTH has not already been initiated. - parent::auth(); - - $this->_send('AUTH CRAM-MD5'); - $challenge = $this->_expect(334); - $challenge = base64_decode($challenge); - $digest = $this->_hmacMd5($this->_password, $challenge); - $this->_send(base64_encode($this->_username . ' ' . $digest)); - $this->_expect(235); - $this->_auth = true; - } - - protected function _hmacMd5($key, $data, $block = 64) - { - if (strlen($key) > 64) { - $key = pack('H32', md5($key)); - } elseif (strlen($key) < 64) { - $key = str_pad($key, $block, 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; - } -} diff --git a/mail/protocol/smtp/auth/login.php b/mail/protocol/smtp/auth/login.php deleted file mode 100644 index 4d24b9f..0000000 --- a/mail/protocol/smtp/auth/login.php +++ /dev/null @@ -1,35 +0,0 @@ -_username = $config['username']; - } - if (isset($config['password'])) { - $this->_password = $config['password']; - } - } - - parent::__construct($host, $port, $config); - } - - public function auth() - { - // Ensure AUTH has not already been initiated. - parent::auth(); - - $this->_send('AUTH LOGIN'); - $this->_expect(334); - $this->_send(base64_encode($this->_username)); - $this->_expect(334); - $this->_send(base64_encode($this->_password)); - $this->_expect(235); - $this->_auth = true; - } -} diff --git a/mail/protocol/smtp/auth/plain.php b/mail/protocol/smtp/auth/plain.php deleted file mode 100644 index 38ed235..0000000 --- a/mail/protocol/smtp/auth/plain.php +++ /dev/null @@ -1,33 +0,0 @@ -_username = $config['username']; - } - if (isset($config['password'])) { - $this->_password = $config['password']; - } - } - - parent::__construct($host, $port, $config); - } - - public function auth() - { - // Ensure AUTH has not already been initiated. - parent::auth(); - - $this->_send('AUTH PLAIN'); - $this->_expect(334); - $this->_send(base64_encode(chr(0) . $this->_username . chr(0) . $this->_password)); - $this->_expect(235); - $this->_auth = true; - } -} diff --git a/mail/transport/abstract.php b/mail/transport/abstract.php deleted file mode 100644 index ea98e19..0000000 --- a/mail/transport/abstract.php +++ /dev/null @@ -1,179 +0,0 @@ -_mail->getType(); - if (!$type) { - if ($this->_mail->hasAttachments) { - $type = IPF_Mime::MULTIPART_MIXED; - } elseif ($this->_mail->getBodyText() && $this->_mail->getBodyHtml()) { - $type = IPF_Mime::MULTIPART_ALTERNATIVE; - } else { - $type = IPF_Mime::MULTIPART_MIXED; - } - } - - $this->_headers['Content-Type'] = array( - $type . '; charset="' . $this->_mail->getCharset() . '";' - . $this->EOL - . " " . 'boundary="' . $boundary . '"' - ); - $this->_headers['MIME-Version'] = array('1.0'); - - $this->boundary = $boundary; - } - - return $this->_headers; - } - - protected function _prepareHeaders($headers) - { - if (!$this->_mail) { - throw new IPF_Mail_Exception('Missing IPF_Mail object in _mail property'); - } - - $this->header = ''; - - foreach ($headers as $header => $content) { - if (isset($content['append'])) { - unset($content['append']); - $value = implode(',' . $this->EOL . ' ', $content); - $this->header .= $header . ': ' . $value . $this->EOL; - } else { - foreach ($content as $value) { - $this->header .= $header . ': ' . $value . $this->EOL; - } - } - } - - // Sanity check on headers -- should not be > 998 characters - $sane = true; - foreach (explode($this->EOL, $this->header) as $line) { - if (strlen(trim($line)) > 998) { - $sane = false; - break; - } - } - if (!$sane) { - throw new IPF_Mail_Exception('At least one mail header line is too long'); - } - } - - protected function _buildBody() - { - if (($text = $this->_mail->getBodyText()) - && ($html = $this->_mail->getBodyHtml())) - { - // Generate unique boundary for multipart/alternative - $mime = new IPF_Mime(null); - $boundaryLine = $mime->boundaryLine($this->EOL); - $boundaryEnd = $mime->mimeEnd($this->EOL); - - $text->disposition = false; - $html->disposition = false; - - $body = $boundaryLine - . $text->getHeaders($this->EOL) - . $this->EOL - . $text->getContent($this->EOL) - . $this->EOL - . $boundaryLine - . $html->getHeaders($this->EOL) - . $this->EOL - . $html->getContent($this->EOL) - . $this->EOL - . $boundaryEnd; - - $mp = new IPF_Mime_Part($body); - $mp->type = IPF_Mime::MULTIPART_ALTERNATIVE; - $mp->boundary = $mime->boundary(); - - $this->_isMultipart = true; - - // Ensure first part contains text alternatives - array_unshift($this->_parts, $mp); - - // Get headers - $this->_headers = $this->_mail->getHeaders(); - return; - } - - // If not multipart, then get the body - if (false !== ($body = $this->_mail->getBodyHtml())) { - array_unshift($this->_parts, $body); - } elseif (false !== ($body = $this->_mail->getBodyText())) { - array_unshift($this->_parts, $body); - } - - if (!$body) { - throw new IPF_Mail_Exception('No body specified'); - } - - // Get headers - $this->_headers = $this->_mail->getHeaders(); - $headers = $body->getHeadersArray($this->EOL); - foreach ($headers as $header) { - // Headers in IPF_Mime_Part are kept as arrays with two elements, a - // key and a value - $this->_headers[$header[0]] = array($header[1]); - } - } - - public function send(IPF_Mail $mail) - { - $this->_isMultipart = false; - $this->_mail = $mail; - $this->_parts = $mail->getParts(); - $mime = $mail->getMime(); - - // Build body content - $this->_buildBody(); - - // Determine number of parts and boundary - $count = count($this->_parts); - $boundary = null; - if ($count < 1) { - throw new IPF_Mail_Exception('Empty mail cannot be sent'); - } - - if ($count > 1) { - // Multipart message; create new MIME object and boundary - $mime = new IPF_Mime($this->_mail->getMimeBoundary()); - $boundary = $mime->boundary(); - } elseif ($this->_isMultipart) { - // multipart/alternative -- grab boundary - $boundary = $this->_parts[0]->boundary; - } - - // Determine recipients, and prepare headers - $this->recipients = implode(',', $mail->getRecipients()); - $this->_prepareHeaders($this->_getHeaders($boundary)); - - // Create message body - // This is done so that the same IPF_Mail object can be used in - // multiple transports - $message = new IPF_Mime_Message(); - $message->setParts($this->_parts); - $message->setMime($mime); - $this->body = $message->generateMessage($this->EOL); - - // Send to transport! - $this->_sendMail(); - } -} diff --git a/mail/transport/sendmail.php b/mail/transport/sendmail.php deleted file mode 100644 index 6a16fc1..0000000 --- a/mail/transport/sendmail.php +++ /dev/null @@ -1,71 +0,0 @@ -parameters = $parameters; - } - - public function _sendMail() - { - if ($this->parameters === null) { - $result = mail( - $this->recipients, - $this->_mail->getSubject(), - $this->body, - $this->header); - } else { - $result = mail( - $this->recipients, - $this->_mail->getSubject(), - $this->body, - $this->header, - $this->parameters); - } - if (!$result) { - throw new IPF_Mail_Exception('Unable to send mail'); - } - } - - protected function _prepareHeaders($headers) - { - if (!$this->_mail) { - throw new IPF_Mail_Exception('_prepareHeaders requires a registered IPF_Mail object'); - } - - // mail() uses its $to parameter to set the To: header, and the $subject - // parameter to set the Subject: header. We need to strip them out. - if (0 === strpos(PHP_OS, 'WIN')) { - // If the current recipients list is empty, throw an error - if (empty($this->recipients)) { - throw new IPF_Mail_Exception('Missing To addresses'); - } - } else { - // All others, simply grab the recipients and unset the To: header - if (!isset($headers['To'])) { - throw new IPF_Mail_Exception('Missing To header'); - } - - unset($headers['To']['append']); - $this->recipients = implode(',', $headers['To']); - } - - // Remove recipient header - unset($headers['To']); - - // Remove subject header, if present - if (isset($headers['Subject'])) { - unset($headers['Subject']); - } - - // Prepare headers - parent::_prepareHeaders($headers); - } - -} - diff --git a/mail/transport/smtp.php b/mail/transport/smtp.php deleted file mode 100644 index e099a4d..0000000 --- a/mail/transport/smtp.php +++ /dev/null @@ -1,91 +0,0 @@ -_name = $config['name']; - } - if (isset($config['port'])) { - $this->_port = $config['port']; - } - if (isset($config['auth'])) { - $this->_auth = $config['auth']; - } - - $this->_host = $host; - $this->_config = $config; - } - - public function __destruct() - { - if ($this->_connection instanceof IPF_Mail_Protocol_Smtp) { - try { - $this->_connection->quit(); - } catch (IPF_Mail_Exception $e) { - // ignore - } - $this->_connection->disconnect(); - } - } - - public function setConnection(IPF_Mail_Protocol_Abstract $connection) - { - $this->_connection = $connection; - } - - public function getConnection() - { - return $this->_connection; - } - - public function _sendMail() - { - // If sending multiple messages per session use existing adapter - if (!($this->_connection instanceof IPF_Mail_Protocol_Smtp)) { - // Check if authentication is required and determine required class - $connectionClass = 'IPF_Mail_Protocol_Smtp'; - if ($this->_auth) { - $connectionClass .= '_Auth_' . ucwords($this->_auth); - } - $this->setConnection(new $connectionClass($this->_host, $this->_port, $this->_config)); - $this->_connection->connect(); - $this->_connection->helo($this->_name); - } else { - // Reset connection to ensure reliable transaction - $this->_connection->rset(); - } - - // Set mail return path from sender email address - $this->_connection->mail($this->_mail->getReturnPath()); - - // Set recipient forward paths - foreach ($this->_mail->getRecipients() as $recipient) { - $this->_connection->rcpt($recipient); - } - - // Issue DATA command to client - $this->_connection->data($this->header . IPF_Mime::LINEEND . $this->body); - } - - protected function _prepareHeaders($headers) - { - if (!$this->_mail) { - throw new IPF_Mail_Exception('_prepareHeaders requires a registered IPF_Mail object'); - } - - unset($headers['Bcc']); - - // Prepare headers - parent::_prepareHeaders($headers); - } -} diff --git a/mime.php b/mime.php deleted file mode 100644 index f303360..0000000 --- a/mime.php +++ /dev/null @@ -1,205 +0,0 @@ - $lineLength) { - $ptr = $lineLength; - } - - // Ensure we are not splitting across an encoded character - $pos = strrpos(substr($str, 0, $ptr), '='); - if ($pos !== false && $pos >= $ptr - 2) { - $ptr = $pos; - } - - // Check if there is a space at the end of the line and rewind - if ($ptr > 0 && $str[$ptr - 1] == ' ') { - --$ptr; - } - - // Add string and continue - $out .= substr($str, 0, $ptr) . '=' . $lineEnd; - $str = substr($str, $ptr); - } - - $out = rtrim($out, $lineEnd); - $out = rtrim($out, '='); - return $out; - } - - public static function encodeQ($str) - { - $str = str_replace('=', '=3D', $str); - $str = str_replace(self::$qpKeys, self::$qpReplaceValues, $str); - $str = str_replace(array('?', '_'), array('=3F', '=5F'), $str); - $str = str_replace(' ', '_', $str); - - $result = array(); - - // Split encoded text into separate lines - $lineLength = 75 - 12; - $pos = 0; - $length = strlen($str); - while ($pos < $length) - { - if ($length - $pos > $lineLength) - { - $ptr = $lineLength; - - // Ensure we are not splitting across an encoded character - if (substr($str, $pos + $ptr - 2, 1) == '=') - $ptr -= 2; - elseif (substr($str, $pos + $ptr - 1, 1) == '=') - $ptr -= 1; - - // Add string and continue - $out = substr($str, $pos, $ptr); - $pos += $ptr; - } - else - { - $out = substr($str, $pos); - $pos = $length; - } - $result[] = '=?utf-8?Q?' . $out . '?='; - } - - return implode("\r\n ", $result); - } - - public static function encodeBase64($str, - $lineLength = self::LINELENGTH, - $lineEnd = self::LINEEND) - { - return rtrim(chunk_split(base64_encode($str), $lineLength, $lineEnd)); - } - - public function __construct($boundary = null) - { - // This string needs to be somewhat unique - if ($boundary === null) { - $this->_boundary = '=_' . md5(microtime(1) . self::$makeUnique++); - } else { - $this->_boundary = $boundary; - } - } - - public static function encode($str, $encoding, $EOL = self::LINEEND) - { - switch ($encoding) { - case self::ENCODING_BASE64: - return self::encodeBase64($str, self::LINELENGTH, $EOL); - - case self::ENCODING_QUOTEDPRINTABLE: - return self::encodeQuotedPrintable($str, self::LINELENGTH, $EOL); - - default: - /** - * @todo 7Bit and 8Bit is currently handled the same way. - */ - return $str; - } - } - - public function boundary() - { - return $this->_boundary; - } - - public function boundaryLine($EOL = self::LINEEND) - { - return $EOL . '--' . $this->_boundary . $EOL; - } - - public function mimeEnd($EOL = self::LINEEND) - { - return $EOL . '--' . $this->_boundary . '--' . $EOL; - } -} diff --git a/mime/decode.php b/mime/decode.php deleted file mode 100644 index e4a5d42..0000000 --- a/mime/decode.php +++ /dev/null @@ -1,153 +0,0 @@ - $headers, - 'body' => $body ); - } - return $result; - } - - public static function splitMessage($message, &$headers, &$body, $EOL = IPF_Mime::LINEEND) - { - // check for valid header at first line - $firstline = strtok($message, "\n"); - if (!preg_match('%^[^\s]+[^:]*:%', $firstline)) { - $headers = array(); - // TODO: we're ignoring \r for now - is this function fast enough and is it safe to asume noone needs \r? - $body = str_replace(array("\r", "\n"), array('', $EOL), $message); - return; - } - - // find an empty line between headers and body - // default is set new line - if (strpos($message, $EOL . $EOL)) { - list($headers, $body) = explode($EOL . $EOL, $message, 2); - // next is the standard new line - } else if ($EOL != "\r\n" && strpos($message, "\r\n\r\n")) { - list($headers, $body) = explode("\r\n\r\n", $message, 2); - // next is the other "standard" new line - } else if ($EOL != "\n" && strpos($message, "\n\n")) { - list($headers, $body) = explode("\n\n", $message, 2); - // at last resort find anything that looks like a new line - } else { - @list($headers, $body) = @preg_split("%([\r\n]+)\\1%U", $message, 2); - } - - $headers = iconv_mime_decode_headers($headers, ICONV_MIME_DECODE_CONTINUE_ON_ERROR); - - // normalize header names - foreach ($headers as $name => $header) { - $lower = strtolower($name); - if ($lower == $name) { - continue; - } - unset($headers[$name]); - if (!isset($headers[$lower])) { - $headers[$lower] = $header; - continue; - } - if (is_array($headers[$lower])) { - $headers[$lower][] = $header; - continue; - } - $headers[$lower] = array($headers[$lower], $header); - } - } - - public static function splitContentType($type, $wantedPart = null) - { - return self::splitHeaderField($type, $wantedPart, 'type'); - } - - public static function splitHeaderField($field, $wantedPart = null, $firstName = 0) - { - $wantedPart = strtolower($wantedPart); - $firstName = strtolower($firstName); - - // special case - a bit optimized - if ($firstName === $wantedPart) { - $field = strtok($field, ';'); - return $field[0] == '"' ? substr($field, 1, -1) : $field; - } - - $field = $firstName . '=' . $field; - if (!preg_match_all('%([^=\s]+)\s*=("[^"]+"|[^;]+)(;\s*|$)%', $field, $matches)) { - throw new IPF_Mime_Exception('not a valid header field'); - } - - if ($wantedPart) { - foreach ($matches[1] as $key => $name) { - if (strcasecmp($name, $wantedPart)) { - continue; - } - if ($matches[2][$key][0] != '"') { - return $matches[2][$key]; - } - return substr($matches[2][$key], 1, -1); - } - return null; - } - - $split = array(); - foreach ($matches[1] as $key => $name) { - $name = strtolower($name); - if ($matches[2][$key][0] == '"') { - $split[$name] = substr($matches[2][$key], 1, -1); - } else { - $split[$name] = $matches[2][$key]; - } - } - - return $split; - } - - public static function decodeQuotedPrintable($string) - { - return iconv_mime_decode($string, ICONV_MIME_DECODE_CONTINUE_ON_ERROR); - } -} diff --git a/mime/exception.php b/mime/exception.php deleted file mode 100644 index 54d46df..0000000 --- a/mime/exception.php +++ /dev/null @@ -1,6 +0,0 @@ -_parts; - } - - public function setParts($parts) - { - $this->_parts = $parts; - } - - public function addPart(IPF_Mime_Part $part) - { - /** - * @todo check for duplicate object handle - */ - $this->_parts[] = $part; - } - - public function isMultiPart() - { - return (count($this->_parts) > 1); - } - - public function setMime(IPF_Mime $mime) - { - $this->_mime = $mime; - } - - public function getMime() - { - if ($this->_mime === null) { - $this->_mime = new IPF_Mime(); - } - - return $this->_mime; - } - - public function generateMessage($EOL = IPF_Mime::LINEEND) - { - if (! $this->isMultiPart()) { - $body = array_shift($this->_parts); - $body = $body->getContent($EOL); - } else { - $mime = $this->getMime(); - - $boundaryLine = $mime->boundaryLine($EOL); - $body = 'This is a message in Mime Format. If you see this, ' - . "your mail reader does not support this format." . $EOL; - - foreach (array_keys($this->_parts) as $p) { - $body .= $boundaryLine - . $this->getPartHeaders($p, $EOL) - . $EOL - . $this->getPartContent($p, $EOL); - } - - $body .= $mime->mimeEnd($EOL); - } - - return trim($body); - } - - public function getPartHeadersArray($partnum) - { - return $this->_parts[$partnum]->getHeadersArray(); - } - - public function getPartHeaders($partnum, $EOL = IPF_Mime::LINEEND) - { - return $this->_parts[$partnum]->getHeaders($EOL); - } - - public function getPartContent($partnum, $EOL = IPF_Mime::LINEEND) - { - return $this->_parts[$partnum]->getContent($EOL); - } - - protected static function _disassembleMime($body, $boundary) - { - $start = 0; - $res = array(); - // find every mime part limiter and cut out the - // string before it. - // the part before the first boundary string is discarded: - $p = strpos($body, '--'.$boundary."\n", $start); - if ($p === false) { - // no parts found! - return array(); - } - - // position after first boundary line - $start = $p + 3 + strlen($boundary); - - while (($p = strpos($body, '--' . $boundary . "\n", $start)) !== false) { - $res[] = substr($body, $start, $p-$start); - $start = $p + 3 + strlen($boundary); - } - - // no more parts, find end boundary - $p = strpos($body, '--' . $boundary . '--', $start); - if ($p===false) { - throw new IPF_Mime_Exception('Not a valid Mime Message: End Missing'); - } - - // the remaining part also needs to be parsed: - $res[] = substr($body, $start, $p-$start); - return $res; - } - - public static function createFromMessage($message, $boundary, $EOL = IPF_Mime::LINEEND) - { - $parts = IPF_Mime_Decode::splitMessageStruct($message, $boundary, $EOL); - - $res = new self(); - foreach ($parts as $part) { - // now we build a new MimePart for the current Message Part: - $newPart = new IPF_Mime_Part($part); - foreach ($part['header'] as $key => $value) { - /** - * @todo check for characterset and filename - */ - // list($key, $value) = $header; - switch($key) { - case 'content-type': - $newPart->type = $value; - break; - case 'content-transfer-encoding': - $newPart->encoding = $value; - break; - case 'content-id': - $newPart->id = trim($value,'<>'); - break; - case 'Content-Disposition': - $newPart->disposition = $value; - break; - case 'content-description': - $newPart->description = $value; - break; - default: - throw new IPF_Mime_Exception('Unknown header ignored for MimePart:' . $key); - } - } - $res->addPart($newPart); - } - return $res; - } -} diff --git a/mime/part.php b/mime/part.php deleted file mode 100644 index 1fbc583..0000000 --- a/mime/part.php +++ /dev/null @@ -1,128 +0,0 @@ -_content = $content; - if (is_resource($content)) { - $this->_isStream = true; - } - } - - public function isStream() - { - return $this->_isStream; - } - - public function getEncodedStream() - { - if (!$this->_isStream) { - throw new IPF_Mime_Exception('Attempt to get a stream from a string part'); - } - - //stream_filter_remove(); // ??? is that right? - switch ($this->encoding) { - case IPF_Mime::ENCODING_QUOTEDPRINTABLE: - $filter = stream_filter_append( - $this->_content, - 'convert.quoted-printable-encode', - STREAM_FILTER_READ, - array( - 'line-length' => 72, - 'line-break-chars' => IPF_Mime::LINEEND - ) - ); - if (!is_resource($filter)) { - throw new IPF_Mime_Exception('Failed to append quoted-printable filter'); - } - break; - case IPF_Mime::ENCODING_BASE64: - $filter = stream_filter_append( - $this->_content, - 'convert.base64-encode', - STREAM_FILTER_READ, - array( - 'line-length' => 72, - 'line-break-chars' => IPF_Mime::LINEEND - ) - ); - if (!is_resource($filter)) { - throw new IPF_Mime_Exception('Failed to append base64 filter'); - } - break; - default: - } - return $this->_content; - } - - public function getContent($EOL = IPF_Mime::LINEEND) - { - if ($this->_isStream) { - return stream_get_contents($this->getEncodedStream()); - } else { - return IPF_Mime::encode($this->_content, $this->encoding, $EOL); - } - } - - public function getHeadersArray($EOL = IPF_Mime::LINEEND) - { - $headers = array(); - - $contentType = $this->type; - if ($this->charset) { - $contentType .= '; charset="' . $this->charset . '"'; - } - - if ($this->boundary) { - $contentType .= ';' . $EOL - . " boundary=\"" . $this->boundary . '"'; - } - - $headers[] = array('Content-Type', $contentType); - - if ($this->encoding) { - $headers[] = array('Content-Transfer-Encoding', $this->encoding); - } - - if ($this->id) { - $headers[] = array('Content-ID', '<' . $this->id . '>'); - } - - if ($this->disposition) { - $disposition = $this->disposition; - if ($this->filename) { - $disposition .= '; filename="' . $this->filename . '"'; - } - $headers[] = array('Content-Disposition', $disposition); - } - - if ($this->description) { - $headers[] = array('Content-Description', $this->description); - } - - return $headers; - } - - public function getHeaders($EOL = IPF_Mime::LINEEND) - { - $res = ''; - foreach ($this->getHeadersArray($EOL) as $header) { - $res .= $header[0] . ': ' . $header[1] . $EOL; - } - - return $res; - } -} diff --git a/src/exception.php b/src/exception.php new file mode 100644 index 0000000..ef36fc7 --- /dev/null +++ b/src/exception.php @@ -0,0 +1,6 @@ +_charset = $charset; + } + + public function getCharset() + { + return $this->_charset; + } + + public function setType($type) + { + $allowed = array( + IPF_Mime::MULTIPART_ALTERNATIVE, + IPF_Mime::MULTIPART_MIXED, + IPF_Mime::MULTIPART_RELATED, + ); + if (!in_array($type, $allowed)) { + throw new IPF_Mail_Exception('Invalid content type "' . $type . '"'); + } + + $this->_type = $type; + return $this; + } + + public function getType() + { + return $this->_type; + } + + public function setMimeBoundary($boundary) + { + $this->_mimeBoundary = $boundary; + + return $this; + } + + public function getMimeBoundary() + { + return $this->_mimeBoundary; + } + + public function setBodyText($txt, $charset = null, $encoding = IPF_Mime::ENCODING_QUOTEDPRINTABLE) + { + if ($charset === null) { + $charset = $this->_charset; + } + + $mp = new IPF_Mime_Part($txt); + $mp->encoding = $encoding; + $mp->type = IPF_Mime::TYPE_TEXT; + $mp->disposition = IPF_Mime::DISPOSITION_INLINE; + $mp->charset = $charset; + + $this->_bodyText = $mp; + + return $this; + } + + public function getBodyText($textOnly = false) + { + if ($textOnly && $this->_bodyText) { + $body = $this->_bodyText; + return $body->getContent(); + } + + return $this->_bodyText; + } + + public function setBodyHtml($html, $charset = null, $encoding = IPF_Mime::ENCODING_QUOTEDPRINTABLE) + { + if ($charset === null) { + $charset = $this->_charset; + } + + $mp = new IPF_Mime_Part($html); + $mp->encoding = $encoding; + $mp->type = IPF_Mime::TYPE_HTML; + $mp->disposition = IPF_Mime::DISPOSITION_INLINE; + $mp->charset = $charset; + + $this->_bodyHtml = $mp; + + return $this; + } + + public function getBodyHtml($htmlOnly = false) + { + if ($htmlOnly && $this->_bodyHtml) { + $body = $this->_bodyHtml; + return $body->getContent(); + } + + return $this->_bodyHtml; + } + + public function addAttachment(IPF_Mime_Part $attachment) + { + $this->addPart($attachment); + $this->hasAttachments = true; + + return $this; + } + + public function createAttachment($body, + $mimeType = IPF_Mime::TYPE_OCTETSTREAM, + $disposition = IPF_Mime::DISPOSITION_ATTACHMENT, + $encoding = IPF_Mime::ENCODING_BASE64, + $filename = null) + { + + $mp = new IPF_Mime_Part($body); + $mp->encoding = $encoding; + $mp->type = $mimeType; + $mp->disposition = $disposition; + $mp->filename = $filename; + + $this->addAttachment($mp); + + return $mp; + } + + public function getPartCount() + { + return count($this->_parts); + } + + protected function _encodeHeader($value) + { + if (IPF_Mime::isPrintable($value)) { + return $value; + } else { + $quotedValue = IPF_Mime::encodeQuotedPrintable($value); + $quotedValue = str_replace(array('?', ' '), array('=3F', '=20'), $quotedValue); + return '=?' . $this->_charset . '?Q?' . $quotedValue . '?='; + } + } + + protected function _storeHeader($headerName, $value, $append=false) + { +// ?? $value = strtr($value,"\r\n\t",'???'); + if (isset($this->_headers[$headerName])) { + $this->_headers[$headerName][] = $value; + } else { + $this->_headers[$headerName] = array($value); + } + + if ($append) { + $this->_headers[$headerName]['append'] = true; + } + + } + + protected function _addRecipient($email, $to = false) + { + // prevent duplicates + $this->_recipients[$email] = 1; + + if ($to) { + $this->_to[] = $email; + } + } + + protected function _addRecipientAndHeader($headerName, $name, $email) + { + $email = strtr($email,"\r\n\t",'???'); + $this->_addRecipient($email, ('To' == $headerName) ? true : false); + if ($name != '') { + $name = '"' . $this->_encodeHeader($name) . '" '; + } + + $this->_storeHeader($headerName, $name .'<'. $email . '>', true); + } + + public function addTo($email, $name='') + { + $this->_addRecipientAndHeader('To', $name, $email); + return $this; + } + + public function addCc($email, $name='') + { + $this->_addRecipientAndHeader('Cc', $name, $email); + return $this; + } + + public function addBcc($email) + { + $this->_addRecipientAndHeader('Bcc', '', $email); + return $this; + } + + public function getRecipients() + { + return array_keys($this->_recipients); + } + + public function setFrom($email, $name = '') + { + if ($this->_from === null) { + $email = strtr($email,"\r\n\t",'???'); + $this->_from = $email; + $this->_storeHeader('From', $this->_encodeHeader('"'.$name.'"').' <'.$email.'>', true); + } else { + throw new IPF_Mail_Exception('From Header set twice'); + } + return $this; + } + + public function getFrom() + { + return $this->_from; + } + + public function setReturnPath($email) + { + if ($this->_returnPath === null) { + $email = strtr($email,"\r\n\t",'???'); + $this->_returnPath = $email; + $this->_storeHeader('Return-Path', $email, false); + } else { + throw new IPF_Mail_Exception('Return-Path Header set twice'); + } + return $this; + } + + public function getReturnPath() + { + if (null !== $this->_returnPath) { + return $this->_returnPath; + } + + return $this->_from; + } + + public function setSubject($subject) + { + if ($this->_subject === null) { + $subject = strtr($subject,"\r\n\t", '???'); + if (IPF_Mime::isPrintable($subject)) + $this->_subject = $subject; + else + $this->_subject = IPF_Mime::encodeQ($subject); + $this->_storeHeader('Subject', $this->_subject); + } else { + throw new IPF_Mail_Exception('Subject set twice'); + } + return $this; + } + + public function getSubject() + { + return $this->_subject; + } + + public function setDate($date = null) + { + if ($this->_date === null) { + if ($date === null) { + $date = date('r'); + } else if (is_int($date)) { + $date = date('r', $date); + } else if (is_string($date)) { + $date = strtotime($date); + if ($date === false || $date < 0) { + throw new IPF_Mail_Exception('String representations of Date Header must be ' . + 'strtotime()-compatible'); + } + $date = date('r', $date); + } else { + throw new IPF_Mail_Exception(__METHOD__ . ' only accepts UNIX timestamps and strtotime()-compatible strings'); + } + $this->_date = $date; + $this->_storeHeader('Date', $date); + } else { + throw new IPF_Mail_Exception('Date Header set twice'); + } + return $this; + } + + public function getDate() + { + return $this->_date; + } + + public function addHeader($name, $value, $append = false) + { + if (in_array(strtolower($name), array('to', 'cc', 'bcc', 'from', 'subject', 'return-path', 'date'))) { + throw new IPF_Mail_Exception('Cannot set standard header from addHeader()'); + } + + $value = strtr($value,"\r\n\t",'???'); + $value = $this->_encodeHeader($value); + $this->_storeHeader($name, $value, $append); + + return $this; + } + + public function getHeaders() + { + return $this->_headers; + } + + public function send($transport = null) + { + if ($transport === null) { + if (! self::$_defaultTransport instanceof IPF_Mail_Transport_Abstract) { + $transport = new IPF_Mail_Transport_Sendmail(); + } else { + $transport = self::$_defaultTransport; + } + } + + if (is_null($this->_date)) { + $this->setDate(); + } + + $transport->send($this); + + return $this; + } + +} diff --git a/src/mail/exception.php b/src/mail/exception.php new file mode 100644 index 0000000..8ec3e7a --- /dev/null +++ b/src/mail/exception.php @@ -0,0 +1,6 @@ +_flags = array_combine($params['flags'], $params['flags']); + } + + parent::__construct($params); + } + + public function getTopLines() + { + return $this->_topLines; + } + + public function hasFlag($flag) + { + return isset($this->_flags[$flag]); + } + + public function getFlags() + { + return $this->_flags; + } +} diff --git a/src/mail/part.php b/src/mail/part.php new file mode 100644 index 0000000..f74df14 --- /dev/null +++ b/src/mail/part.php @@ -0,0 +1,238 @@ +_mail = $params['handler']; + $this->_messageNum = $params['id']; + } + + if (isset($params['raw'])) { + IPF_Mime_Decode::splitMessage($params['raw'], $this->_headers, $this->_content); + } else if (isset($params['headers'])) { + if (is_array($params['headers'])) { + $this->_headers = $params['headers']; + } else { + if (!empty($params['noToplines'])) { + IPF_Mime_Decode::splitMessage($params['headers'], $this->_headers, $null); + } else { + IPF_Mime_Decode::splitMessage($params['headers'], $this->_headers, $this->_topLines); + } + } + if (isset($params['content'])) { + $this->_content = $params['content']; + } + } + } + + public function isMultipart() + { + try { + return stripos($this->contentType, 'multipart/') === 0; + } catch(IPF_Mail_Exception $e) { + return false; + } + } + + + public function getContent() + { + if ($this->_content !== null) { + return $this->_content; + } + + if ($this->_mail) { + return $this->_mail->getRawContent($this->_messageNum); + } else { + throw new IPF_Mail_Exception('no content'); + } + } + + protected function _cacheContent() + { + // caching content if we can't fetch parts + if ($this->_content === null && $this->_mail) { + $this->_content = $this->_mail->getRawContent($this->_messageNum); + } + + if (!$this->isMultipart()) { + return; + } + + // split content in parts + $boundary = $this->getHeaderField('content-type', 'boundary'); + if (!$boundary) { + throw new IPF_Mail_Exception('no boundary found in content type to split message'); + } + $parts = IPF_Mime_Decode::splitMessageStruct($this->_content, $boundary); + $counter = 1; + foreach ($parts as $part) { + $this->_parts[$counter++] = new self(array('headers' => $part['header'], 'content' => $part['body'])); + } + } + + public function getPart($num) + { + if (isset($this->_parts[$num])) { + return $this->_parts[$num]; + } + + if (!$this->_mail && $this->_content === null) { + throw new IPF_Mail_Exception('part not found'); + } + + if ($this->_mail && $this->_mail->hasFetchPart) { + // TODO: fetch part + // return + } + + $this->_cacheContent(); + + if (!isset($this->_parts[$num])) { + throw new IPF_Mail_Exception('part not found'); + } + + return $this->_parts[$num]; + } + + public function countParts() + { + if ($this->_countParts) { + return $this->_countParts; + } + + $this->_countParts = count($this->_parts); + if ($this->_countParts) { + return $this->_countParts; + } + + if ($this->_mail && $this->_mail->hasFetchPart) { + // TODO: fetch part + // return + } + + $this->_cacheContent(); + + $this->_countParts = count($this->_parts); + return $this->_countParts; + } + + public function getHeaders() + { + if ($this->_headers === null) { + if (!$this->_mail) { + $this->_headers = array(); + } else { + $part = $this->_mail->getRawHeader($this->_messageNum); + IPF_Mime_Decode::splitMessage($part, $this->_headers, $null); + } + } + + return $this->_headers; + } + + public function getHeader($name, $format = null) + { + if ($this->_headers === null) { + $this->getHeaders(); + } + + $lowerName = strtolower($name); + + if (!isset($this->_headers[$lowerName])) { + $lowerName = strtolower(preg_replace('%([a-z])([A-Z])%', '\1-\2', $name)); + if (!isset($this->_headers[$lowerName])) { + throw new IPF_Mail_Exception("no Header with Name $name found"); + } + } + $name = $lowerName; + + $header = $this->_headers[$name]; + + switch ($format) { + case 'string': + if (is_array($header)) { + $header = implode(IPF_Mime::LINEEND, $header); + } + break; + case 'array': + $header = (array)$header; + default: + // do nothing + } + + return $header; + } + + public function getHeaderField($name, $wantedPart = 0, $firstName = 0) { + return IPF_Mime_Decode::splitHeaderField(current($this->getHeader($name, 'array')), $wantedPart, $firstName); + } + + public function __get($name) + { + return $this->getHeader($name, 'string'); + } + + public function __toString() + { + return $this->getContent(); + } + + public function hasChildren() + { + $current = $this->current(); + return $current && $current instanceof IPF_Mail_Part && $current->isMultipart(); + } + + public function getChildren() + { + return $this->current(); + } + + public function valid() + { + if ($this->_countParts === null) { + $this->countParts(); + } + return $this->_iterationPos && $this->_iterationPos <= $this->_countParts; + } + + public function next() + { + ++$this->_iterationPos; + } + + public function key() + { + return $this->_iterationPos; + } + + public function current() + { + return $this->getPart($this->_iterationPos); + } + + public function rewind() + { + $this->countParts(); + $this->_iterationPos = 1; + } +} diff --git a/src/mail/protocol/abstract.php b/src/mail/protocol/abstract.php new file mode 100644 index 0000000..88eb17a --- /dev/null +++ b/src/mail/protocol/abstract.php @@ -0,0 +1,147 @@ +_host = $host; + $this->_port = $port; + } + + public function __destruct() + { + $this->_disconnect(); + } + + abstract public function connect(); + public function getRequest() + { + return $this->_request; + } + + public function getResponse() + { + return $this->_response; + } + + public function getLog() + { + return $this->_log; + } + + public function resetLog() + { + $this->_log = ''; + } + + protected function _connect($remote) + { + $errorNum = 0; + $errorStr = ''; + + // open connection + $this->_socket = stream_socket_client($remote, $errorNum, $errorStr, self::TIMEOUT_CONNECTION); + + if ($this->_socket === false) { + if ($errorNum == 0) { + $errorStr = 'Could not open socket'; + } + throw new IPF_Mail_Exception($errorStr); + } + + if (($result = stream_set_timeout($this->_socket, self::TIMEOUT_CONNECTION)) === false) { + throw new IPF_Mail_Exception('Could not set stream timeout'); + } + + return $result; + } + + protected function _disconnect() + { + if (is_resource($this->_socket)) { + fclose($this->_socket); + } + } + + protected function _send($request) + { + if (!is_resource($this->_socket)) { + throw new IPF_Mail_Exception('No connection has been established to ' . $this->_host); + } + + $this->_request = $request; + + $result = fwrite($this->_socket, $request . self::EOL); + + // Save request to internal log + $this->_log .= $request . self::EOL; + + if ($result === false) { + throw new IPF_Mail_Exception('Could not send request to ' . $this->_host); + } + + return $result; + } + + protected function _receive($timeout = null) + { + if (!is_resource($this->_socket)) { + throw new IPF_Mail_Exception('No connection has been established to ' . $this->_host); + } + + // Adapters may wish to supply per-commend timeouts according to appropriate RFC + if ($timeout !== null) { + stream_set_timeout($this->_socket, $timeout); + } + + // Retrieve response + $reponse = fgets($this->_socket, 1024); + + // Save request to internal log + $this->_log .= $reponse; + + // Check meta data to ensure connection is still valid + $info = stream_get_meta_data($this->_socket); + + if (!empty($info['timed_out'])) { + throw new IPF_Mail_Exception($this->_host . ' has timed out'); + } + + if ($reponse === false) { + throw new IPF_Mail_Exception('Could not read from ' . $this->_host); + } + + return $reponse; + } + + protected function _expect($code, $timeout = null) + { + $this->_response = array(); + $cmd = ''; + $msg = ''; + if (!is_array($code)) { + $code = array($code); + } + do { + $this->_response[] = $result = $this->_receive($timeout); + sscanf($result, $this->_template, $cmd, $msg); + + if ($cmd === null || !in_array($cmd, $code)) { + throw new IPF_Mail_Exception($result); + } + + } while (strpos($msg, '-') === 0); // The '-' message prefix indicates an information string instead of a response string. + return $msg; + } +} diff --git a/src/mail/protocol/imap.php b/src/mail/protocol/imap.php new file mode 100644 index 0000000..2e05564 --- /dev/null +++ b/src/mail/protocol/imap.php @@ -0,0 +1,505 @@ +connect($host, $port, $ssl); + } + } + + public function __destruct() + { + $this->logout(); + } + + public function connect($host, $port = null, $ssl = false) + { + if ($ssl == 'SSL') { + $host = 'ssl://' . $host; + } + + if ($port === null) { + $port = $ssl === 'SSL' ? 993 : 143; + } + + $this->_socket = @fsockopen($host, $port); + if (!$this->_socket) { + throw new IPF_Mail_Exception('cannot connect to host'); + } + + if (!$this->_assumedNextLine('* OK')) { + throw new IPF_Mail_Exception('host doesn\'t allow connection'); + } + + if ($ssl === 'TLS') { + $result = $this->requestAndResponse('STARTTLS'); + $result = $result && stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + if (!$result) { + throw new IPF_Mail_Exception('cannot enable TLS'); + } + } + } + + protected function _nextLine() + { + $line = @fgets($this->_socket); + if ($line === false) { + throw new IPF_Mail_Exception('cannot read - connection closed?'); + } + + return $line; + } + + protected function _assumedNextLine($start) + { + $line = $this->_nextLine(); + return strpos($line, $start) === 0; + } + + protected function _nextTaggedLine(&$tag) + { + $line = $this->_nextLine(); + + // seperate tag from line + list($tag, $line) = explode(' ', $line, 2); + + return $line; + } + + protected function _decodeLine($line) + { + $tokens = array(); + $stack = array(); + + /* + We start to decode the response here. The unterstood tokens are: + literal + "literal" or also "lit\\er\"al" + {bytes}literal + (literals*) + All tokens are returned in an array. Literals in braces (the last unterstood + token in the list) are returned as an array of tokens. I.e. the following response: + "foo" baz {3}bar ("f\\\"oo" bar) + would be returned as: + array('foo', 'baz', 'bar', array('f\\\"oo', 'bar')); + + // TODO: add handling of '[' and ']' to parser for easier handling of response text + */ + // replace any trailling including spaces with a single space + $line = rtrim($line) . ' '; + while (($pos = strpos($line, ' ')) !== false) { + $token = substr($line, 0, $pos); + while ($token[0] == '(') { + array_push($stack, $tokens); + $tokens = array(); + $token = substr($token, 1); + } + if ($token[0] == '"') { + if (preg_match('%^"((.|\\\\|\\")*?)" *%', $line, $matches)) { + $tokens[] = $matches[1]; + $line = substr($line, strlen($matches[0])); + continue; + } + } + if ($token[0] == '{') { + $endPos = strpos($token, '}'); + $chars = substr($token, 1, $endPos - 1); + if (is_numeric($chars)) { + $token = ''; + while (strlen($token) < $chars) { + $token .= $this->_nextLine(); + } + $line = ''; + if (strlen($token) > $chars) { + $line = substr($token, $chars); + $token = substr($token, 0, $chars); + } else { + $line .= $this->_nextLine(); + } + $tokens[] = $token; + $line = trim($line) . ' '; + continue; + } + } + if ($stack && $token[strlen($token) - 1] == ')') { + // closing braces are not seperated by spaces, so we need to count them + $braces = strlen($token); + $token = rtrim($token, ')'); + // only count braces if more than one + $braces -= strlen($token) + 1; + // only add if token had more than just closing braces + if ($token) { + $tokens[] = $token; + } + $token = $tokens; + $tokens = array_pop($stack); + // special handline if more than one closing brace + while ($braces-- > 0) { + $tokens[] = $token; + $token = $tokens; + $tokens = array_pop($stack); + } + } + $tokens[] = $token; + $line = substr($line, $pos + 1); + } + + // maybe the server forgot to send some closing braces + while ($stack) { + $child = $tokens; + $tokens = array_pop($stack); + $tokens[] = $child; + } + + return $tokens; + } + + public function readLine(&$tokens = array(), $wantedTag = '*', $dontParse = false) + { + $line = $this->_nextTaggedLine($tag); + if (!$dontParse) { + $tokens = $this->_decodeLine($line); + } else { + $tokens = $line; + } + + // if tag is wanted tag we might be at the end of a multiline response + return $tag == $wantedTag; + } + + public function readResponse($tag, $dontParse = false) + { + $lines = array(); + while (!$this->readLine($tokens, $tag, $dontParse)) { + $lines[] = $tokens; + } + + if ($dontParse) { + // last to chars are still needed for response code + $tokens = array(substr($tokens, 0, 2)); + } + // last line has response code + if ($tokens[0] == 'OK') { + return $lines ? $lines : true; + } else if ($tokens[0] == 'NO'){ + return false; + } + return null; + } + + public function sendRequest($command, $tokens = array(), &$tag = null) + { + if (!$tag) { + ++$this->_tagCount; + $tag = 'TAG' . $this->_tagCount; + } + + $line = $tag . ' ' . $command; + + foreach ($tokens as $token) { + if (is_array($token)) { + if (@fputs($this->_socket, $line . ' ' . $token[0] . "\r\n") === false) { + throw new IPF_Mail_Exception('cannot write - connection closed?'); + } + if (!$this->_assumedNextLine('+ ')) { + throw new IPF_Mail_Exception('cannot send literal string'); + } + $line = $token[1]; + } else { + $line .= ' ' . $token; + } + } + + if (@fputs($this->_socket, $line . "\r\n") === false) { + throw new IPF_Mail_Exception('cannot write - connection closed?'); + } + } + + public function requestAndResponse($command, $tokens = array(), $dontParse = false) + { + $this->sendRequest($command, $tokens, $tag); + $response = $this->readResponse($tag, $dontParse); + + return $response; + } + + public function escapeString($string) + { + if (func_num_args() < 2) { + if (strpos($string, "\n") !== false) { + return array('{' . strlen($string) . '}', $string); + } else { + return '"' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $string) . '"'; + } + } + $result = array(); + foreach (func_get_args() as $string) { + $result[] = $this->escapeString($string); + } + return $result; + } + + public function escapeList($list) + { + $result = array(); + foreach ($list as $v) { + if (is_array($v)) { + $result[] = $this->escapeList($v); + } else { +// $result[] = $this->escapeString($v); + $result[] = $v; + } + } + return '(' . implode(' ', $result) . ')'; + } + + public function login($user, $password) + { + return $this->requestAndResponse('LOGIN', $this->escapeString($user, $password), true); + } + + public function logout() + { + $result = false; + if ($this->_socket) { + try { + $result = $this->requestAndResponse('LOGOUT', array(), true); + } catch (IPF_Mail_Exception $e) { + // ignoring exception + } + fclose($this->_socket); + $this->_socket = null; + } + return $result; + } + + public function capability() + { + $response = $this->requestAndResponse('CAPABILITY'); + + if (!$response) { + return $response; + } + + $capabilities = array(); + foreach ($response as $line) { + $capabilities = array_merge($capabilities, $line); + } + return $capabilities; + } + + public function examineOrSelect($command = 'EXAMINE', $box = 'INBOX') + { + $this->sendRequest($command, array($this->escapeString($box)), $tag); + + $result = array(); + while (!$this->readLine($tokens, $tag)) { + if ($tokens[0] == 'FLAGS') { + array_shift($tokens); + $result['flags'] = $tokens; + continue; + } + switch ($tokens[1]) { + case 'EXISTS': + case 'RECENT': + $result[strtolower($tokens[1])] = $tokens[0]; + break; + case '[UIDVALIDITY': + $result['uidvalidity'] = (int)$tokens[2]; + break; + default: + // ignore + } + } + + if ($tokens[0] != 'OK') { + return false; + } + return $result; + } + + public function select($box = 'INBOX') + { + return $this->examineOrSelect('SELECT', $box); + } + + public function examine($box = 'INBOX') + { + return $this->examineOrSelect('EXAMINE', $box); + } + + public function fetch($items, $from, $to = null) + { + if (is_array($from)) { + $set = implode(',', $from); + } else if ($to === null) { + $set = (int)$from; + } else if ($to === INF) { + $set = (int)$from . ':*'; + } else { + $set = (int)$from . ':' . (int)$to; + } + + $items = (array)$items; + $itemList = $this->escapeList($items); + + $this->sendRequest('FETCH', array($set, $itemList), $tag); + + $result = array(); + while (!$this->readLine($tokens, $tag)) { + // ignore other responses + if ($tokens[1] != 'FETCH') { + continue; + } + // ignore other messages + if ($to === null && !is_array($from) && $tokens[0] != $from) { + continue; + } + // if we only want one item we return that one directly + if (count($items) == 1) { + if ($tokens[2][0] == $items[0]) { + $data = $tokens[2][1]; + } else { + // maybe the server send an other field we didn't wanted + $count = count($tokens[2]); + // we start with 2, because 0 was already checked + for ($i = 2; $i < $count; $i += 2) { + if ($tokens[2][$i] != $items[0]) { + continue; + } + $data = $tokens[2][$i + 1]; + break; + } + } + } else { + $data = array(); + while (key($tokens[2]) !== null) { + $data[current($tokens[2])] = next($tokens[2]); + next($tokens[2]); + } + } + // if we want only one message we can ignore everything else and just return + if ($to === null && !is_array($from) && $tokens[0] == $from) { + // we still need to read all lines + while (!$this->readLine($tokens, $tag)); + return $data; + } + $result[$tokens[0]] = $data; + } + + if ($to === null && !is_array($from)) { + throw new IPF_Mail_Exception('the single id was not found in response'); + } + + return $result; + } + + public function listMailbox($reference = '', $mailbox = '*') + { + $result = array(); + $list = $this->requestAndResponse('LIST', $this->escapeString($reference, $mailbox)); + if (!$list || $list === true) { + return $result; + } + + foreach ($list as $item) { + if (count($item) != 4 || $item[0] != 'LIST') { + continue; + } + $result[$item[3]] = array('delim' => $item[2], 'flags' => $item[1]); + } + + return $result; + } + + public function store(array $flags, $from, $to = null, $mode = null, $silent = true) + { + $item = 'FLAGS'; + if ($mode == '+' || $mode == '-') { + $item = $mode . $item; + } + if ($silent) { + $item .= '.SILENT'; + } + + $flags = $this->escapeList($flags); + $set = (int)$from; + if ($to != null) { + $set .= ':' . ($to == INF ? '*' : (int)$to); + } + + $result = $this->requestAndResponse('STORE', array($set, $item, $flags), $silent); + + if ($silent) { + return $result ? true : false; + } + + $tokens = $result; + $result = array(); + foreach ($tokens as $token) { + if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') { + continue; + } + $result[$token[0]] = $token[2][1]; + } + + return $result; + } + + public function append($folder, $message, $flags = null, $date = null) + { + $tokens = array(); + $tokens[] = $this->escapeString($folder); + if ($flags !== null) { + $tokens[] = $this->escapeList($flags); + } + if ($date !== null) { + $tokens[] = $this->escapeString($date); + } + $tokens[] = $this->escapeString($message); + + return $this->requestAndResponse('APPEND', $tokens, true); + } + + public function copy($folder, $from, $to = null) + { + $set = (int)$from; + if ($to != null) { + $set .= ':' . ($to == INF ? '*' : (int)$to); + } + + return $this->requestAndResponse('COPY', array($set, $this->escapeString($folder)), true); + } + + public function create($folder) + { + return $this->requestAndResponse('CREATE', array($this->escapeString($folder)), true); + } + + public function rename($old, $new) + { + return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true); + } + + public function delete($folder) + { + return $this->requestAndResponse('DELETE', array($this->escapeString($folder)), true); + } + + public function expunge() + { + // TODO: parse response? + return $this->requestAndResponse('EXPUNGE'); + } + + public function noop() + { + // TODO: parse response + return $this->requestAndResponse('NOOP'); + } +} diff --git a/src/mail/protocol/pop3.php b/src/mail/protocol/pop3.php new file mode 100644 index 0000000..cd148c9 --- /dev/null +++ b/src/mail/protocol/pop3.php @@ -0,0 +1,246 @@ +connect($host, $port, $ssl); + } + } + + public function __destruct() + { + $this->logout(); + } + + public function connect($host, $port = null, $ssl = false) + { + if ($ssl == 'SSL') { + $host = 'ssl://' . $host; + } + + if ($port === null) { + $port = $ssl == 'SSL' ? 995 : 110; + } + + $this->_socket = @fsockopen($host, $port); + if (!$this->_socket) { + throw new IPF_Mail_Exception('cannot connect to host'); + } + + $welcome = $this->readResponse(); + + strtok($welcome, '<'); + $this->_timestamp = strtok('>'); + if (!strpos($this->_timestamp, '@')) { + $this->_timestamp = null; + } else { + $this->_timestamp = '<' . $this->_timestamp . '>'; + } + + if ($ssl === 'TLS') { + $this->request('STLS'); + $result = stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + if (!$result) { + throw new IPF_Mail_Exception('cannot enable TLS'); + } + } + + return $welcome; + } + + public function sendRequest($request) + { + $result = @fputs($this->_socket, $request . "\r\n"); + if (!$result) { + throw new IPF_Mail_Exception('send failed - connection closed?'); + } + } + + public function readResponse($multiline = false) + { + $result = @fgets($this->_socket); + if (!is_string($result)) { + throw new IPF_Mail_Exception('read failed - connection closed?'); + } + + $result = trim($result); + if (strpos($result, ' ')) { + list($status, $message) = explode(' ', $result, 2); + } else { + $status = $result; + $message = ''; + } + + if ($status != '+OK') { + throw new IPF_Mail_Exception('last request failed'); + } + + if ($multiline) { + $message = ''; + $line = fgets($this->_socket); + while ($line && trim($line) != '.') { + $message .= $line; + $line = fgets($this->_socket); + }; + } + + return $message; + } + + public function request($request, $multiline = false) + { + $this->sendRequest($request); + return $this->readResponse($multiline); + } + + public function logout() + { + if (!$this->_socket) { + return; + } + + try { + $this->request('QUIT'); + } catch (IPF_Mail_Exception $e) { + // ignore error - we're closing the socket anyway + } + + fclose($this->_socket); + $this->_socket = null; + } + + public function capa() + { + $result = $this->request('CAPA', true); + return explode("\n", $result); + } + + public function login($user, $password, $tryApop = true) + { + if ($tryApop && $this->_timestamp) { + try { + $this->request("APOP $user " . md5($this->_timestamp . $password)); + return; + } catch (IPF_Mail_Exception $e) { + // ignore + } + } + + $result = $this->request("USER $user"); + $result = $this->request("PASS $password"); + } + + public function status(&$messages, &$octets) + { + $messages = 0; + $octets = 0; + $result = $this->request('STAT'); + + list($messages, $octets) = explode(' ', $result); + } + + public function getList($msgno = null) + { + if ($msgno !== null) { + $result = $this->request("LIST $msgno"); + + list(, $result) = explode(' ', $result); + return (int)$result; + } + + $result = $this->request('LIST', true); + $messages = array(); + $line = strtok($result, "\n"); + while ($line) { + list($no, $size) = explode(' ', trim($line)); + $messages[(int)$no] = (int)$size; + $line = strtok("\n"); + } + + return $messages; + } + + public function uniqueid($msgno = null) + { + if ($msgno !== null) { + $result = $this->request("UIDL $msgno"); + + list(, $result) = explode(' ', $result); + return $result; + } + + $result = $this->request('UIDL', true); + + $result = explode("\n", $result); + $messages = array(); + foreach ($result as $line) { + if (!$line) { + continue; + } + list($no, $id) = explode(' ', trim($line), 2); + $messages[(int)$no] = $id; + } + + return $messages; + + } + + public function top($msgno, $lines = 0, $fallback = false) + { + if ($this->hasTop === false) { + if ($fallback) { + return $this->retrieve($msgno); + } else { + throw new IPF_Mail_Exception('top not supported and no fallback wanted'); + } + } + $this->hasTop = true; + + $lines = (!$lines || $lines < 1) ? 0 : (int)$lines; + + try { + $result = $this->request("TOP $msgno $lines", true); + } catch (IPF_Mail_Exception $e) { + $this->hasTop = false; + if ($fallback) { + $result = $this->retrieve($msgno); + } else { + throw $e; + } + } + + return $result; + } + + public function retrive($msgno) + { + return $this->retrieve($msgno); + } + + public function retrieve($msgno) + { + $result = $this->request("RETR $msgno", true); + return $result; + } + + public function noop() + { + $this->request('NOOP'); + } + + public function delete($msgno) + { + $this->request("DELE $msgno"); + } + + public function undelete() + { + $this->request('RSET'); + } +} diff --git a/src/mail/protocol/smtp.php b/src/mail/protocol/smtp.php new file mode 100644 index 0000000..008f26f --- /dev/null +++ b/src/mail/protocol/smtp.php @@ -0,0 +1,199 @@ +_secure = 'tls'; + break; + + case 'ssl': + $this->_transport = 'ssl'; + $this->_secure = 'ssl'; + if ($port == null) { + $port = 465; + } + break; + + default: + throw new IPF_Mail_Exception($config['ssl'] . ' is unsupported SSL type'); + break; + } + } + + // If no port has been specified then check the master PHP ini file. Defaults to 25 if the ini setting is null. + if ($port == null) { + if (($port = ini_get('smtp_port')) == '') { + $port = 25; + } + } + + parent::__construct($host, $port); + } + + + public function connect() + { + return $this->_connect($this->_transport . '://' . $this->_host . ':'. $this->_port); + } + + public function helo($host = '127.0.0.1') + { + // Respect RFC 2821 and disallow HELO attempts if session is already initiated. + if ($this->_sess === true) { + throw new IPF_Mail_Exception('Cannot issue HELO to existing session'); + } + + // Validate client hostname + if (!$this->_validHost->isValid($host)) { + throw new IPF_Mail_Exception(join(', ', $this->_validHost->getMessage())); + } + + // Initiate helo sequence + $this->_expect(220, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + $this->_ehlo($host); + + // If a TLS session is required, commence negotiation + if ($this->_secure == 'tls') { + $this->_send('STARTTLS'); + $this->_expect(220, 180); + if (!stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { + throw new IPF_Mail_Exception('Unable to connect via TLS'); + } + $this->_ehlo($host); + } + + $this->_startSession(); + $this->auth(); + } + + protected function _ehlo($host) + { + // Support for older, less-compliant remote servers. Tries multiple attempts of EHLO or HELO. + try { + $this->_send('EHLO ' . $host); + $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + } catch (IPF_Mail_Exception $e) { + $this->_send('HELO ' . $host); + $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + } catch (IPF_Mail_Exception $e) { + throw $e; + } + } + + public function mail($from) + { + if ($this->_sess !== true) { + throw new IPF_Mail_Exception('A valid session has not been started'); + } + + $this->_send('MAIL FROM:<' . $from . '>'); + $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + + // Set mail to true, clear recipients and any existing data flags as per 4.1.1.2 of RFC 2821 + $this->_mail = true; + $this->_rcpt = false; + $this->_data = false; + } + + public function rcpt($to) + { + if ($this->_mail !== true) { + throw new IPF_Mail_Exception('No sender reverse path has been supplied'); + } + + // Set rcpt to true, as per 4.1.1.3 of RFC 2821 + $this->_send('RCPT TO:<' . $to . '>'); + $this->_expect(array(250, 251), 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + $this->_rcpt = true; + } + + public function data($data) + { + // Ensure recipients have been set + if ($this->_rcpt !== true) { + throw new IPF_Mail_Exception('No recipient forward path has been supplied'); + } + + $this->_send('DATA'); + $this->_expect(354, 120); // Timeout set for 2 minutes as per RFC 2821 4.5.3.2 + + foreach (explode(IPF_Mime::LINEEND, $data) as $line) { + if (strpos($line, '.') === 0) { + // Escape lines prefixed with a '.' + $line = '.' . $line; + } + $this->_send($line); + } + + $this->_send('.'); + $this->_expect(250, 600); // Timeout set for 10 minutes as per RFC 2821 4.5.3.2 + $this->_data = true; + } + + public function rset() + { + $this->_send('RSET'); + // MS ESMTP doesn't follow RFC, see [ZF-1377] + $this->_expect(array(250, 220)); + + $this->_mail = false; + $this->_rcpt = false; + $this->_data = false; + } + + public function noop() + { + $this->_send('NOOP'); + $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + } + + public function vrfy($user) + { + $this->_send('VRFY ' . $user); + $this->_expect(array(250, 251, 252), 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + } + + public function quit() + { + if ($this->_sess) { + $this->_send('QUIT'); + $this->_expect(221, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2 + $this->_stopSession(); + } + } + + public function auth() + { + if ($this->_auth === true) { + throw new IPF_Mail_Exception('Already authenticated for this session'); + } + } + + public function disconnect() + { + $this->_disconnect(); + } + + protected function _startSession() + { + $this->_sess = true; + } + + protected function _stopSession() + { + $this->_sess = false; + } +} diff --git a/src/mail/protocol/smtp/auth/crammd5.php b/src/mail/protocol/smtp/auth/crammd5.php new file mode 100644 index 0000000..576033e --- /dev/null +++ b/src/mail/protocol/smtp/auth/crammd5.php @@ -0,0 +1,49 @@ +_username = $config['username']; + } + if (isset($config['password'])) { + $this->_password = $config['password']; + } + } + + parent::__construct($host, $port, $config); + } + + public function auth() + { + // Ensure AUTH has not already been initiated. + parent::auth(); + + $this->_send('AUTH CRAM-MD5'); + $challenge = $this->_expect(334); + $challenge = base64_decode($challenge); + $digest = $this->_hmacMd5($this->_password, $challenge); + $this->_send(base64_encode($this->_username . ' ' . $digest)); + $this->_expect(235); + $this->_auth = true; + } + + protected function _hmacMd5($key, $data, $block = 64) + { + if (strlen($key) > 64) { + $key = pack('H32', md5($key)); + } elseif (strlen($key) < 64) { + $key = str_pad($key, $block, 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; + } +} diff --git a/src/mail/protocol/smtp/auth/login.php b/src/mail/protocol/smtp/auth/login.php new file mode 100644 index 0000000..4d24b9f --- /dev/null +++ b/src/mail/protocol/smtp/auth/login.php @@ -0,0 +1,35 @@ +_username = $config['username']; + } + if (isset($config['password'])) { + $this->_password = $config['password']; + } + } + + parent::__construct($host, $port, $config); + } + + public function auth() + { + // Ensure AUTH has not already been initiated. + parent::auth(); + + $this->_send('AUTH LOGIN'); + $this->_expect(334); + $this->_send(base64_encode($this->_username)); + $this->_expect(334); + $this->_send(base64_encode($this->_password)); + $this->_expect(235); + $this->_auth = true; + } +} diff --git a/src/mail/protocol/smtp/auth/plain.php b/src/mail/protocol/smtp/auth/plain.php new file mode 100644 index 0000000..38ed235 --- /dev/null +++ b/src/mail/protocol/smtp/auth/plain.php @@ -0,0 +1,33 @@ +_username = $config['username']; + } + if (isset($config['password'])) { + $this->_password = $config['password']; + } + } + + parent::__construct($host, $port, $config); + } + + public function auth() + { + // Ensure AUTH has not already been initiated. + parent::auth(); + + $this->_send('AUTH PLAIN'); + $this->_expect(334); + $this->_send(base64_encode(chr(0) . $this->_username . chr(0) . $this->_password)); + $this->_expect(235); + $this->_auth = true; + } +} diff --git a/src/mail/transport/abstract.php b/src/mail/transport/abstract.php new file mode 100644 index 0000000..ea98e19 --- /dev/null +++ b/src/mail/transport/abstract.php @@ -0,0 +1,179 @@ +_mail->getType(); + if (!$type) { + if ($this->_mail->hasAttachments) { + $type = IPF_Mime::MULTIPART_MIXED; + } elseif ($this->_mail->getBodyText() && $this->_mail->getBodyHtml()) { + $type = IPF_Mime::MULTIPART_ALTERNATIVE; + } else { + $type = IPF_Mime::MULTIPART_MIXED; + } + } + + $this->_headers['Content-Type'] = array( + $type . '; charset="' . $this->_mail->getCharset() . '";' + . $this->EOL + . " " . 'boundary="' . $boundary . '"' + ); + $this->_headers['MIME-Version'] = array('1.0'); + + $this->boundary = $boundary; + } + + return $this->_headers; + } + + protected function _prepareHeaders($headers) + { + if (!$this->_mail) { + throw new IPF_Mail_Exception('Missing IPF_Mail object in _mail property'); + } + + $this->header = ''; + + foreach ($headers as $header => $content) { + if (isset($content['append'])) { + unset($content['append']); + $value = implode(',' . $this->EOL . ' ', $content); + $this->header .= $header . ': ' . $value . $this->EOL; + } else { + foreach ($content as $value) { + $this->header .= $header . ': ' . $value . $this->EOL; + } + } + } + + // Sanity check on headers -- should not be > 998 characters + $sane = true; + foreach (explode($this->EOL, $this->header) as $line) { + if (strlen(trim($line)) > 998) { + $sane = false; + break; + } + } + if (!$sane) { + throw new IPF_Mail_Exception('At least one mail header line is too long'); + } + } + + protected function _buildBody() + { + if (($text = $this->_mail->getBodyText()) + && ($html = $this->_mail->getBodyHtml())) + { + // Generate unique boundary for multipart/alternative + $mime = new IPF_Mime(null); + $boundaryLine = $mime->boundaryLine($this->EOL); + $boundaryEnd = $mime->mimeEnd($this->EOL); + + $text->disposition = false; + $html->disposition = false; + + $body = $boundaryLine + . $text->getHeaders($this->EOL) + . $this->EOL + . $text->getContent($this->EOL) + . $this->EOL + . $boundaryLine + . $html->getHeaders($this->EOL) + . $this->EOL + . $html->getContent($this->EOL) + . $this->EOL + . $boundaryEnd; + + $mp = new IPF_Mime_Part($body); + $mp->type = IPF_Mime::MULTIPART_ALTERNATIVE; + $mp->boundary = $mime->boundary(); + + $this->_isMultipart = true; + + // Ensure first part contains text alternatives + array_unshift($this->_parts, $mp); + + // Get headers + $this->_headers = $this->_mail->getHeaders(); + return; + } + + // If not multipart, then get the body + if (false !== ($body = $this->_mail->getBodyHtml())) { + array_unshift($this->_parts, $body); + } elseif (false !== ($body = $this->_mail->getBodyText())) { + array_unshift($this->_parts, $body); + } + + if (!$body) { + throw new IPF_Mail_Exception('No body specified'); + } + + // Get headers + $this->_headers = $this->_mail->getHeaders(); + $headers = $body->getHeadersArray($this->EOL); + foreach ($headers as $header) { + // Headers in IPF_Mime_Part are kept as arrays with two elements, a + // key and a value + $this->_headers[$header[0]] = array($header[1]); + } + } + + public function send(IPF_Mail $mail) + { + $this->_isMultipart = false; + $this->_mail = $mail; + $this->_parts = $mail->getParts(); + $mime = $mail->getMime(); + + // Build body content + $this->_buildBody(); + + // Determine number of parts and boundary + $count = count($this->_parts); + $boundary = null; + if ($count < 1) { + throw new IPF_Mail_Exception('Empty mail cannot be sent'); + } + + if ($count > 1) { + // Multipart message; create new MIME object and boundary + $mime = new IPF_Mime($this->_mail->getMimeBoundary()); + $boundary = $mime->boundary(); + } elseif ($this->_isMultipart) { + // multipart/alternative -- grab boundary + $boundary = $this->_parts[0]->boundary; + } + + // Determine recipients, and prepare headers + $this->recipients = implode(',', $mail->getRecipients()); + $this->_prepareHeaders($this->_getHeaders($boundary)); + + // Create message body + // This is done so that the same IPF_Mail object can be used in + // multiple transports + $message = new IPF_Mime_Message(); + $message->setParts($this->_parts); + $message->setMime($mime); + $this->body = $message->generateMessage($this->EOL); + + // Send to transport! + $this->_sendMail(); + } +} diff --git a/src/mail/transport/sendmail.php b/src/mail/transport/sendmail.php new file mode 100644 index 0000000..6a16fc1 --- /dev/null +++ b/src/mail/transport/sendmail.php @@ -0,0 +1,71 @@ +parameters = $parameters; + } + + public function _sendMail() + { + if ($this->parameters === null) { + $result = mail( + $this->recipients, + $this->_mail->getSubject(), + $this->body, + $this->header); + } else { + $result = mail( + $this->recipients, + $this->_mail->getSubject(), + $this->body, + $this->header, + $this->parameters); + } + if (!$result) { + throw new IPF_Mail_Exception('Unable to send mail'); + } + } + + protected function _prepareHeaders($headers) + { + if (!$this->_mail) { + throw new IPF_Mail_Exception('_prepareHeaders requires a registered IPF_Mail object'); + } + + // mail() uses its $to parameter to set the To: header, and the $subject + // parameter to set the Subject: header. We need to strip them out. + if (0 === strpos(PHP_OS, 'WIN')) { + // If the current recipients list is empty, throw an error + if (empty($this->recipients)) { + throw new IPF_Mail_Exception('Missing To addresses'); + } + } else { + // All others, simply grab the recipients and unset the To: header + if (!isset($headers['To'])) { + throw new IPF_Mail_Exception('Missing To header'); + } + + unset($headers['To']['append']); + $this->recipients = implode(',', $headers['To']); + } + + // Remove recipient header + unset($headers['To']); + + // Remove subject header, if present + if (isset($headers['Subject'])) { + unset($headers['Subject']); + } + + // Prepare headers + parent::_prepareHeaders($headers); + } + +} + diff --git a/src/mail/transport/smtp.php b/src/mail/transport/smtp.php new file mode 100644 index 0000000..e099a4d --- /dev/null +++ b/src/mail/transport/smtp.php @@ -0,0 +1,91 @@ +_name = $config['name']; + } + if (isset($config['port'])) { + $this->_port = $config['port']; + } + if (isset($config['auth'])) { + $this->_auth = $config['auth']; + } + + $this->_host = $host; + $this->_config = $config; + } + + public function __destruct() + { + if ($this->_connection instanceof IPF_Mail_Protocol_Smtp) { + try { + $this->_connection->quit(); + } catch (IPF_Mail_Exception $e) { + // ignore + } + $this->_connection->disconnect(); + } + } + + public function setConnection(IPF_Mail_Protocol_Abstract $connection) + { + $this->_connection = $connection; + } + + public function getConnection() + { + return $this->_connection; + } + + public function _sendMail() + { + // If sending multiple messages per session use existing adapter + if (!($this->_connection instanceof IPF_Mail_Protocol_Smtp)) { + // Check if authentication is required and determine required class + $connectionClass = 'IPF_Mail_Protocol_Smtp'; + if ($this->_auth) { + $connectionClass .= '_Auth_' . ucwords($this->_auth); + } + $this->setConnection(new $connectionClass($this->_host, $this->_port, $this->_config)); + $this->_connection->connect(); + $this->_connection->helo($this->_name); + } else { + // Reset connection to ensure reliable transaction + $this->_connection->rset(); + } + + // Set mail return path from sender email address + $this->_connection->mail($this->_mail->getReturnPath()); + + // Set recipient forward paths + foreach ($this->_mail->getRecipients() as $recipient) { + $this->_connection->rcpt($recipient); + } + + // Issue DATA command to client + $this->_connection->data($this->header . IPF_Mime::LINEEND . $this->body); + } + + protected function _prepareHeaders($headers) + { + if (!$this->_mail) { + throw new IPF_Mail_Exception('_prepareHeaders requires a registered IPF_Mail object'); + } + + unset($headers['Bcc']); + + // Prepare headers + parent::_prepareHeaders($headers); + } +} diff --git a/src/mime.php b/src/mime.php new file mode 100644 index 0000000..f303360 --- /dev/null +++ b/src/mime.php @@ -0,0 +1,205 @@ + $lineLength) { + $ptr = $lineLength; + } + + // Ensure we are not splitting across an encoded character + $pos = strrpos(substr($str, 0, $ptr), '='); + if ($pos !== false && $pos >= $ptr - 2) { + $ptr = $pos; + } + + // Check if there is a space at the end of the line and rewind + if ($ptr > 0 && $str[$ptr - 1] == ' ') { + --$ptr; + } + + // Add string and continue + $out .= substr($str, 0, $ptr) . '=' . $lineEnd; + $str = substr($str, $ptr); + } + + $out = rtrim($out, $lineEnd); + $out = rtrim($out, '='); + return $out; + } + + public static function encodeQ($str) + { + $str = str_replace('=', '=3D', $str); + $str = str_replace(self::$qpKeys, self::$qpReplaceValues, $str); + $str = str_replace(array('?', '_'), array('=3F', '=5F'), $str); + $str = str_replace(' ', '_', $str); + + $result = array(); + + // Split encoded text into separate lines + $lineLength = 75 - 12; + $pos = 0; + $length = strlen($str); + while ($pos < $length) + { + if ($length - $pos > $lineLength) + { + $ptr = $lineLength; + + // Ensure we are not splitting across an encoded character + if (substr($str, $pos + $ptr - 2, 1) == '=') + $ptr -= 2; + elseif (substr($str, $pos + $ptr - 1, 1) == '=') + $ptr -= 1; + + // Add string and continue + $out = substr($str, $pos, $ptr); + $pos += $ptr; + } + else + { + $out = substr($str, $pos); + $pos = $length; + } + $result[] = '=?utf-8?Q?' . $out . '?='; + } + + return implode("\r\n ", $result); + } + + public static function encodeBase64($str, + $lineLength = self::LINELENGTH, + $lineEnd = self::LINEEND) + { + return rtrim(chunk_split(base64_encode($str), $lineLength, $lineEnd)); + } + + public function __construct($boundary = null) + { + // This string needs to be somewhat unique + if ($boundary === null) { + $this->_boundary = '=_' . md5(microtime(1) . self::$makeUnique++); + } else { + $this->_boundary = $boundary; + } + } + + public static function encode($str, $encoding, $EOL = self::LINEEND) + { + switch ($encoding) { + case self::ENCODING_BASE64: + return self::encodeBase64($str, self::LINELENGTH, $EOL); + + case self::ENCODING_QUOTEDPRINTABLE: + return self::encodeQuotedPrintable($str, self::LINELENGTH, $EOL); + + default: + /** + * @todo 7Bit and 8Bit is currently handled the same way. + */ + return $str; + } + } + + public function boundary() + { + return $this->_boundary; + } + + public function boundaryLine($EOL = self::LINEEND) + { + return $EOL . '--' . $this->_boundary . $EOL; + } + + public function mimeEnd($EOL = self::LINEEND) + { + return $EOL . '--' . $this->_boundary . '--' . $EOL; + } +} diff --git a/src/mime/decode.php b/src/mime/decode.php new file mode 100644 index 0000000..e4a5d42 --- /dev/null +++ b/src/mime/decode.php @@ -0,0 +1,153 @@ + $headers, + 'body' => $body ); + } + return $result; + } + + public static function splitMessage($message, &$headers, &$body, $EOL = IPF_Mime::LINEEND) + { + // check for valid header at first line + $firstline = strtok($message, "\n"); + if (!preg_match('%^[^\s]+[^:]*:%', $firstline)) { + $headers = array(); + // TODO: we're ignoring \r for now - is this function fast enough and is it safe to asume noone needs \r? + $body = str_replace(array("\r", "\n"), array('', $EOL), $message); + return; + } + + // find an empty line between headers and body + // default is set new line + if (strpos($message, $EOL . $EOL)) { + list($headers, $body) = explode($EOL . $EOL, $message, 2); + // next is the standard new line + } else if ($EOL != "\r\n" && strpos($message, "\r\n\r\n")) { + list($headers, $body) = explode("\r\n\r\n", $message, 2); + // next is the other "standard" new line + } else if ($EOL != "\n" && strpos($message, "\n\n")) { + list($headers, $body) = explode("\n\n", $message, 2); + // at last resort find anything that looks like a new line + } else { + @list($headers, $body) = @preg_split("%([\r\n]+)\\1%U", $message, 2); + } + + $headers = iconv_mime_decode_headers($headers, ICONV_MIME_DECODE_CONTINUE_ON_ERROR); + + // normalize header names + foreach ($headers as $name => $header) { + $lower = strtolower($name); + if ($lower == $name) { + continue; + } + unset($headers[$name]); + if (!isset($headers[$lower])) { + $headers[$lower] = $header; + continue; + } + if (is_array($headers[$lower])) { + $headers[$lower][] = $header; + continue; + } + $headers[$lower] = array($headers[$lower], $header); + } + } + + public static function splitContentType($type, $wantedPart = null) + { + return self::splitHeaderField($type, $wantedPart, 'type'); + } + + public static function splitHeaderField($field, $wantedPart = null, $firstName = 0) + { + $wantedPart = strtolower($wantedPart); + $firstName = strtolower($firstName); + + // special case - a bit optimized + if ($firstName === $wantedPart) { + $field = strtok($field, ';'); + return $field[0] == '"' ? substr($field, 1, -1) : $field; + } + + $field = $firstName . '=' . $field; + if (!preg_match_all('%([^=\s]+)\s*=("[^"]+"|[^;]+)(;\s*|$)%', $field, $matches)) { + throw new IPF_Mime_Exception('not a valid header field'); + } + + if ($wantedPart) { + foreach ($matches[1] as $key => $name) { + if (strcasecmp($name, $wantedPart)) { + continue; + } + if ($matches[2][$key][0] != '"') { + return $matches[2][$key]; + } + return substr($matches[2][$key], 1, -1); + } + return null; + } + + $split = array(); + foreach ($matches[1] as $key => $name) { + $name = strtolower($name); + if ($matches[2][$key][0] == '"') { + $split[$name] = substr($matches[2][$key], 1, -1); + } else { + $split[$name] = $matches[2][$key]; + } + } + + return $split; + } + + public static function decodeQuotedPrintable($string) + { + return iconv_mime_decode($string, ICONV_MIME_DECODE_CONTINUE_ON_ERROR); + } +} diff --git a/src/mime/exception.php b/src/mime/exception.php new file mode 100644 index 0000000..54d46df --- /dev/null +++ b/src/mime/exception.php @@ -0,0 +1,6 @@ +_parts; + } + + public function setParts($parts) + { + $this->_parts = $parts; + } + + public function addPart(IPF_Mime_Part $part) + { + /** + * @todo check for duplicate object handle + */ + $this->_parts[] = $part; + } + + public function isMultiPart() + { + return (count($this->_parts) > 1); + } + + public function setMime(IPF_Mime $mime) + { + $this->_mime = $mime; + } + + public function getMime() + { + if ($this->_mime === null) { + $this->_mime = new IPF_Mime(); + } + + return $this->_mime; + } + + public function generateMessage($EOL = IPF_Mime::LINEEND) + { + if (! $this->isMultiPart()) { + $body = array_shift($this->_parts); + $body = $body->getContent($EOL); + } else { + $mime = $this->getMime(); + + $boundaryLine = $mime->boundaryLine($EOL); + $body = 'This is a message in Mime Format. If you see this, ' + . "your mail reader does not support this format." . $EOL; + + foreach (array_keys($this->_parts) as $p) { + $body .= $boundaryLine + . $this->getPartHeaders($p, $EOL) + . $EOL + . $this->getPartContent($p, $EOL); + } + + $body .= $mime->mimeEnd($EOL); + } + + return trim($body); + } + + public function getPartHeadersArray($partnum) + { + return $this->_parts[$partnum]->getHeadersArray(); + } + + public function getPartHeaders($partnum, $EOL = IPF_Mime::LINEEND) + { + return $this->_parts[$partnum]->getHeaders($EOL); + } + + public function getPartContent($partnum, $EOL = IPF_Mime::LINEEND) + { + return $this->_parts[$partnum]->getContent($EOL); + } + + protected static function _disassembleMime($body, $boundary) + { + $start = 0; + $res = array(); + // find every mime part limiter and cut out the + // string before it. + // the part before the first boundary string is discarded: + $p = strpos($body, '--'.$boundary."\n", $start); + if ($p === false) { + // no parts found! + return array(); + } + + // position after first boundary line + $start = $p + 3 + strlen($boundary); + + while (($p = strpos($body, '--' . $boundary . "\n", $start)) !== false) { + $res[] = substr($body, $start, $p-$start); + $start = $p + 3 + strlen($boundary); + } + + // no more parts, find end boundary + $p = strpos($body, '--' . $boundary . '--', $start); + if ($p===false) { + throw new IPF_Mime_Exception('Not a valid Mime Message: End Missing'); + } + + // the remaining part also needs to be parsed: + $res[] = substr($body, $start, $p-$start); + return $res; + } + + public static function createFromMessage($message, $boundary, $EOL = IPF_Mime::LINEEND) + { + $parts = IPF_Mime_Decode::splitMessageStruct($message, $boundary, $EOL); + + $res = new self(); + foreach ($parts as $part) { + // now we build a new MimePart for the current Message Part: + $newPart = new IPF_Mime_Part($part); + foreach ($part['header'] as $key => $value) { + /** + * @todo check for characterset and filename + */ + // list($key, $value) = $header; + switch($key) { + case 'content-type': + $newPart->type = $value; + break; + case 'content-transfer-encoding': + $newPart->encoding = $value; + break; + case 'content-id': + $newPart->id = trim($value,'<>'); + break; + case 'Content-Disposition': + $newPart->disposition = $value; + break; + case 'content-description': + $newPart->description = $value; + break; + default: + throw new IPF_Mime_Exception('Unknown header ignored for MimePart:' . $key); + } + } + $res->addPart($newPart); + } + return $res; + } +} diff --git a/src/mime/part.php b/src/mime/part.php new file mode 100644 index 0000000..1fbc583 --- /dev/null +++ b/src/mime/part.php @@ -0,0 +1,128 @@ +_content = $content; + if (is_resource($content)) { + $this->_isStream = true; + } + } + + public function isStream() + { + return $this->_isStream; + } + + public function getEncodedStream() + { + if (!$this->_isStream) { + throw new IPF_Mime_Exception('Attempt to get a stream from a string part'); + } + + //stream_filter_remove(); // ??? is that right? + switch ($this->encoding) { + case IPF_Mime::ENCODING_QUOTEDPRINTABLE: + $filter = stream_filter_append( + $this->_content, + 'convert.quoted-printable-encode', + STREAM_FILTER_READ, + array( + 'line-length' => 72, + 'line-break-chars' => IPF_Mime::LINEEND + ) + ); + if (!is_resource($filter)) { + throw new IPF_Mime_Exception('Failed to append quoted-printable filter'); + } + break; + case IPF_Mime::ENCODING_BASE64: + $filter = stream_filter_append( + $this->_content, + 'convert.base64-encode', + STREAM_FILTER_READ, + array( + 'line-length' => 72, + 'line-break-chars' => IPF_Mime::LINEEND + ) + ); + if (!is_resource($filter)) { + throw new IPF_Mime_Exception('Failed to append base64 filter'); + } + break; + default: + } + return $this->_content; + } + + public function getContent($EOL = IPF_Mime::LINEEND) + { + if ($this->_isStream) { + return stream_get_contents($this->getEncodedStream()); + } else { + return IPF_Mime::encode($this->_content, $this->encoding, $EOL); + } + } + + public function getHeadersArray($EOL = IPF_Mime::LINEEND) + { + $headers = array(); + + $contentType = $this->type; + if ($this->charset) { + $contentType .= '; charset="' . $this->charset . '"'; + } + + if ($this->boundary) { + $contentType .= ';' . $EOL + . " boundary=\"" . $this->boundary . '"'; + } + + $headers[] = array('Content-Type', $contentType); + + if ($this->encoding) { + $headers[] = array('Content-Transfer-Encoding', $this->encoding); + } + + if ($this->id) { + $headers[] = array('Content-ID', '<' . $this->id . '>'); + } + + if ($this->disposition) { + $disposition = $this->disposition; + if ($this->filename) { + $disposition .= '; filename="' . $this->filename . '"'; + } + $headers[] = array('Content-Disposition', $disposition); + } + + if ($this->description) { + $headers[] = array('Content-Description', $this->description); + } + + return $headers; + } + + public function getHeaders($EOL = IPF_Mime::LINEEND) + { + $res = ''; + foreach ($this->getHeadersArray($EOL) as $header) { + $res .= $header[0] . ': ' . $header[1] . $EOL; + } + + return $res; + } +}