--- /dev/null
+<?php
+
+class IPF_Mail extends IPF_Mime_Message
+{
+ protected static $_defaultTransport = null;
+ protected $_charset = null;
+ protected $_headers = array();
+ protected $_from = null;
+ protected $_to = array();
+ protected $_recipients = array();
+ protected $_returnPath = null;
+ protected $_subject = null;
+ protected $_date = null;
+ protected $_bodyText = false;
+ protected $_bodyHtml = false;
+ protected $_mimeBoundary = null;
+ protected $_type = null;
+ public $hasAttachments = false;
+ public static function setDefaultTransport(IPF_Mail_Transport_Abstract $transport)
+ {
+ self::$_defaultTransport = $transport;
+ }
+
+ public function __construct($charset='iso-8859-1')
+ {
+ $this->_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_Exception_Mail('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_Exception_Mail('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_Exception_Mail('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",'???');
+ $this->_subject = $this->_encodeHeader($subject);
+ $this->_storeHeader('Subject', $this->_subject);
+ } else {
+ throw new IPF_Exception_Mail('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_Exception_Mail('String representations of Date Header must be ' .
+ 'strtotime()-compatible');
+ }
+ $date = date('r', $date);
+ } else {
+ throw new IPF_Exception_Mail(__METHOD__ . ' only accepts UNIX timestamps and strtotime()-compatible strings');
+ }
+ $this->_date = $date;
+ $this->_storeHeader('Date', $date);
+ } else {
+ throw new IPF_Exception_Mail('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_Exception_Mail('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;
+ }
+
+}
--- /dev/null
+<?php
+
+class IPF_Mail_Message extends IPF_Mail_Part
+{
+ protected $_flags = array();
+
+ public function __construct(array $params)
+ {
+ if (isset($params['file'])) {
+ if (!is_resource($params['file'])) {
+ $params['raw'] = @file_get_contents($params['file']);
+ if ($params['raw'] === false) {
+ throw new IPF_Exception_Mail('could not open file');
+ }
+ } else {
+ $params['raw'] = stream_get_contents($params['file']);
+ }
+ }
+
+ if (!empty($params['flags'])) {
+ // set key and value to the same value for easy lookup
+ $this->_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;
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mail_Part implements RecursiveIterator
+{
+ protected $_headers;
+ protected $_content;
+ protected $_topLines = '';
+ protected $_parts = array();
+ protected $_countParts;
+ protected $_iterationPos = 1;
+ protected $_mail;
+ protected $_messageNum = 0;
+
+ public function __construct(array $params)
+ {
+ if (isset($params['handler'])) {
+ if (!$params['handler'] instanceof IPF_Mail_Storage_Abstract) {
+ throw new IPF_Exception_Mail('handler is not a valid mail handler');
+ }
+ if (!isset($params['id'])) {
+ throw new IPF_Exception_Mail('need a message id with a handler');
+ }
+
+ $this->_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_Exception_Mail $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_Exception_Mail('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_Exception_Mail('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_Exception_Mail('part not found');
+ }
+
+ if ($this->_mail && $this->_mail->hasFetchPart) {
+ // TODO: fetch part
+ // return
+ }
+
+ $this->_cacheContent();
+
+ if (!isset($this->_parts[$num])) {
+ throw new IPF_Exception_Mail('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_Exception_Mail("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;
+ }
+}
--- /dev/null
+<?php
+
+abstract class IPF_Mail_Protocol_Abstract
+{
+ const EOL = "\r\n";
+ const TIMEOUT_CONNECTION = 30;
+
+ protected $_host;
+ protected $_port;
+ protected $_socket;
+ protected $_request;
+ protected $_response;
+ protected $_template = '%d%s';
+ private $_log;
+
+ public function __construct($host = '127.0.0.1', $port = null)
+ {
+ $this->_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_Exception_Mail($errorStr);
+ }
+
+ if (($result = stream_set_timeout($this->_socket, self::TIMEOUT_CONNECTION)) === false) {
+ throw new IPF_Exception_Mail('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_Exception_Mail('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_Exception_Mail('Could not send request to ' . $this->_host);
+ }
+
+ return $result;
+ }
+
+ protected function _receive($timeout = null)
+ {
+ if (!is_resource($this->_socket)) {
+ throw new IPF_Exception_Mail('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_Exception_Mail($this->_host . ' has timed out');
+ }
+
+ if ($reponse === false) {
+ throw new IPF_Exception_Mail('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_Exception_Mail($result);
+ }
+
+ } while (strpos($msg, '-') === 0); // The '-' message prefix indicates an information string instead of a response string.
+ return $msg;
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mail_Protocol_Imap
+{
+ protected $_socket;
+ protected $_tagCount = 0;
+
+ function __construct($host = '', $port = null, $ssl = false)
+ {
+ if ($host) {
+ $this->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_Exception_Mail('cannot connect to host');
+ }
+
+ if (!$this->_assumedNextLine('* OK')) {
+ throw new IPF_Exception_Mail('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_Exception_Mail('cannot enable TLS');
+ }
+ }
+ }
+
+ protected function _nextLine()
+ {
+ $line = @fgets($this->_socket);
+ if ($line === false) {
+ throw new IPF_Exception_Mail('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}<NL>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}<NL>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 <NL> 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_Exception_Mail('cannot write - connection closed?');
+ }
+ if (!$this->_assumedNextLine('+ ')) {
+ throw new IPF_Exception_Mail('cannot send literal string');
+ }
+ $line = $token[1];
+ } else {
+ $line .= ' ' . $token;
+ }
+ }
+
+ if (@fputs($this->_socket, $line . "\r\n") === false) {
+ throw new IPF_Exception_Mail('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 $k => $v) {
+ if (!is_array($v)) {
+// $result[] = $this->escapeString($v);
+ $result[] = $v;
+ continue;
+ }
+ $result[] = $this->escapeList($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_Exception_Mail $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_Exception_Mail('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');
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mail_Protocol_Pop3
+{
+ public $hasTop = null;
+ protected $_socket;
+ protected $_timestamp;
+
+ public function __construct($host = '', $port = null, $ssl = false)
+ {
+ if ($host) {
+ $this->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_Exception_Mail('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_Exception_Mail('cannot enable TLS');
+ }
+ }
+
+ return $welcome;
+ }
+
+ public function sendRequest($request)
+ {
+ $result = @fputs($this->_socket, $request . "\r\n");
+ if (!$result) {
+ throw new IPF_Exception_Mail('send failed - connection closed?');
+ }
+ }
+
+ public function readResponse($multiline = false)
+ {
+ $result = @fgets($this->_socket);
+ if (!is_string($result)) {
+ throw new IPF_Exception_Mail('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_Exception_Mail('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_Exception_Mail $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_Exception_Mail $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_Exception_Mail('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_Exception_Mail $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');
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mail_Protocol_Smtp extends IPF_Mail_Protocol_Abstract
+{
+ protected $_transport = 'tcp';
+ protected $_secure;
+ protected $_sess = false;
+ protected $_helo = false;
+ protected $_auth = false;
+ protected $_mail = false;
+ protected $_rcpt = false;
+ protected $_data = null;
+
+ public function __construct($host = '127.0.0.1', $port = null, $config = array())
+ {
+ if (isset($config['ssl'])) {
+ switch (strtolower($config['ssl'])) {
+ case 'tls':
+ $this->_secure = 'tls';
+ break;
+
+ case 'ssl':
+ $this->_transport = 'ssl';
+ $this->_secure = 'ssl';
+ if ($port == null) {
+ $port = 465;
+ }
+ break;
+
+ default:
+ throw new IPF_Exception_Mail($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_Exception_Mail('Cannot issue HELO to existing session');
+ }
+
+ // Validate client hostname
+ if (!$this->_validHost->isValid($host)) {
+ throw new IPF_Exception_Mail(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_Exception_Mail('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_Exception_Mail $e) {
+ $this->_send('HELO ' . $host);
+ $this->_expect(250, 300); // Timeout set for 5 minutes as per RFC 2821 4.5.3.2
+ } catch (IPF_Exception_Mail $e) {
+ throw $e;
+ }
+ }
+
+ public function mail($from)
+ {
+ if ($this->_sess !== true) {
+ throw new IPF_Exception_Mail('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_Exception_Mail('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_Exception_Mail('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_Exception_Mail('Already authenticated for this session');
+ }
+ }
+
+ public function disconnect()
+ {
+ $this->_disconnect();
+ }
+
+ protected function _startSession()
+ {
+ $this->_sess = true;
+ }
+
+ protected function _stopSession()
+ {
+ $this->_sess = false;
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mail_Protocol_Smtp_Auth_Crammd5 extends IPF_Mail_Protocol_Smtp
+{
+ public function __construct($host = '127.0.0.1', $port = null, $config = null)
+ {
+ if (is_array($config)) {
+ if (isset($config['username'])) {
+ $this->_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;
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mail_Protocol_Smtp_Auth_Login extends IPF_Mail_Protocol_Smtp
+{
+ protected $_username;
+ protected $_password;
+
+ public function __construct($host = '127.0.0.1', $port = null, $config = null)
+ {
+ if (is_array($config)) {
+ if (isset($config['username'])) {
+ $this->_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;
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mail_Protocol_Smtp_Auth_Plain extends IPF_Mail_Protocol_Smtp
+{
+ protected $_username;
+ protected $_password;
+
+ public function __construct($host = '127.0.0.1', $port = null, $config = null)
+ {
+ if (is_array($config)) {
+ if (isset($config['username'])) {
+ $this->_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;
+ }
+}
--- /dev/null
+<?php
+
+abstract class IPF_Mail_Transport_Abstract
+{
+ public $body = '';
+ public $boundary = '';
+ public $header = '';
+ protected $_headers = array();
+ protected $_isMultipart = false;
+ protected $_mail = false;
+ protected $_parts = array();
+ public $recipients = '';
+ public $EOL = "\r\n";
+
+ abstract protected function _sendMail();
+
+ protected function _getHeaders($boundary)
+ {
+ if (null !== $boundary) {
+ // Build multipart mail
+ $type = $this->_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 static function _formatHeader(&$item, $key, $prefix)
+ {
+ $item = $prefix . ': ' . $item;
+ }
+
+ protected function _prepareHeaders($headers)
+ {
+ if (!$this->_mail) {
+ throw new IPF_Exception_Mail('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 {
+ array_walk($content, array(get_class($this), '_formatHeader'), $header);
+ $this->header .= implode($this->EOL, $content) . $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_Exception_Mail('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_Exception_Mail('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_Exception_Mail('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();
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mail_Transport_Sendmail extends IPF_Mail_Transport_Abstract
+{
+ public $subject = null;
+ public $parameters;
+ public $EOL = PHP_EOL;
+
+ public function __construct($parameters = null)
+ {
+ $this->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_Exception_Mail('Unable to send mail');
+ }
+ }
+
+ protected function _prepareHeaders($headers)
+ {
+ if (!$this->_mail) {
+ throw new IPF_Exception_Mail('_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_Exception_Mail('Missing To addresses');
+ }
+ } else {
+ // All others, simply grab the recipients and unset the To: header
+ if (!isset($headers['To'])) {
+ throw new IPF_Exception_Mail('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);
+ }
+
+}
+
--- /dev/null
+<?php
+
+class IPF_Mail_Transport_Smtp extends IPF_Mail_Transport_Abstract
+{
+ public $EOL = "\n";
+ protected $_host;
+ protected $_port;
+ protected $_name = 'localhost';
+ protected $_auth;
+ protected $_config;
+ protected $_connection;
+
+ public function __construct($host = '127.0.0.1', $config = array())
+ {
+ if (isset($config['name'])) {
+ $this->_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_Exception_Mail $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_Exception_Mail('_prepareHeaders requires a registered IPF_Mail object');
+ }
+
+ unset($headers['Bcc']);
+
+ // Prepare headers
+ parent::_prepareHeaders($headers);
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mime
+{
+ const TYPE_OCTETSTREAM = 'application/octet-stream';
+ const TYPE_TEXT = 'text/plain';
+ const TYPE_HTML = 'text/html';
+ const ENCODING_7BIT = '7bit';
+ const ENCODING_8BIT = '8bit';
+ const ENCODING_QUOTEDPRINTABLE = 'quoted-printable';
+ const ENCODING_BASE64 = 'base64';
+ const DISPOSITION_ATTACHMENT = 'attachment';
+ const DISPOSITION_INLINE = 'inline';
+ const LINELENGTH = 74;
+ const LINEEND = "\n";
+ const MULTIPART_ALTERNATIVE = 'multipart/alternative';
+ const MULTIPART_MIXED = 'multipart/mixed';
+ const MULTIPART_RELATED = 'multipart/related';
+
+ protected $_boundary;
+ protected static $makeUnique = 0;
+
+ // lookup-Tables for QuotedPrintable
+ public static $qpKeys = array(
+ "\x00","\x01","\x02","\x03","\x04","\x05","\x06","\x07",
+ "\x08","\x09","\x0A","\x0B","\x0C","\x0D","\x0E","\x0F",
+ "\x10","\x11","\x12","\x13","\x14","\x15","\x16","\x17",
+ "\x18","\x19","\x1A","\x1B","\x1C","\x1D","\x1E","\x1F",
+ "\x7F","\x80","\x81","\x82","\x83","\x84","\x85","\x86",
+ "\x87","\x88","\x89","\x8A","\x8B","\x8C","\x8D","\x8E",
+ "\x8F","\x90","\x91","\x92","\x93","\x94","\x95","\x96",
+ "\x97","\x98","\x99","\x9A","\x9B","\x9C","\x9D","\x9E",
+ "\x9F","\xA0","\xA1","\xA2","\xA3","\xA4","\xA5","\xA6",
+ "\xA7","\xA8","\xA9","\xAA","\xAB","\xAC","\xAD","\xAE",
+ "\xAF","\xB0","\xB1","\xB2","\xB3","\xB4","\xB5","\xB6",
+ "\xB7","\xB8","\xB9","\xBA","\xBB","\xBC","\xBD","\xBE",
+ "\xBF","\xC0","\xC1","\xC2","\xC3","\xC4","\xC5","\xC6",
+ "\xC7","\xC8","\xC9","\xCA","\xCB","\xCC","\xCD","\xCE",
+ "\xCF","\xD0","\xD1","\xD2","\xD3","\xD4","\xD5","\xD6",
+ "\xD7","\xD8","\xD9","\xDA","\xDB","\xDC","\xDD","\xDE",
+ "\xDF","\xE0","\xE1","\xE2","\xE3","\xE4","\xE5","\xE6",
+ "\xE7","\xE8","\xE9","\xEA","\xEB","\xEC","\xED","\xEE",
+ "\xEF","\xF0","\xF1","\xF2","\xF3","\xF4","\xF5","\xF6",
+ "\xF7","\xF8","\xF9","\xFA","\xFB","\xFC","\xFD","\xFE",
+ "\xFF"
+ );
+
+ public static $qpReplaceValues = array(
+ "=00","=01","=02","=03","=04","=05","=06","=07",
+ "=08","=09","=0A","=0B","=0C","=0D","=0E","=0F",
+ "=10","=11","=12","=13","=14","=15","=16","=17",
+ "=18","=19","=1A","=1B","=1C","=1D","=1E","=1F",
+ "=7F","=80","=81","=82","=83","=84","=85","=86",
+ "=87","=88","=89","=8A","=8B","=8C","=8D","=8E",
+ "=8F","=90","=91","=92","=93","=94","=95","=96",
+ "=97","=98","=99","=9A","=9B","=9C","=9D","=9E",
+ "=9F","=A0","=A1","=A2","=A3","=A4","=A5","=A6",
+ "=A7","=A8","=A9","=AA","=AB","=AC","=AD","=AE",
+ "=AF","=B0","=B1","=B2","=B3","=B4","=B5","=B6",
+ "=B7","=B8","=B9","=BA","=BB","=BC","=BD","=BE",
+ "=BF","=C0","=C1","=C2","=C3","=C4","=C5","=C6",
+ "=C7","=C8","=C9","=CA","=CB","=CC","=CD","=CE",
+ "=CF","=D0","=D1","=D2","=D3","=D4","=D5","=D6",
+ "=D7","=D8","=D9","=DA","=DB","=DC","=DD","=DE",
+ "=DF","=E0","=E1","=E2","=E3","=E4","=E5","=E6",
+ "=E7","=E8","=E9","=EA","=EB","=EC","=ED","=EE",
+ "=EF","=F0","=F1","=F2","=F3","=F4","=F5","=F6",
+ "=F7","=F8","=F9","=FA","=FB","=FC","=FD","=FE",
+ "=FF"
+ );
+
+ public static $qpKeysString =
+ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF";
+
+ public static function isPrintable($str)
+ {
+ return (strcspn($str, self::$qpKeysString) == strlen($str));
+ }
+
+ public static function encodeQuotedPrintable($str,
+ $lineLength = self::LINELENGTH,
+ $lineEnd = self::LINEEND)
+ {
+ $out = '';
+ $str = str_replace('=', '=3D', $str);
+ $str = str_replace(self::$qpKeys, self::$qpReplaceValues, $str);
+ $str = rtrim($str);
+
+ // Split encoded text into separate lines
+ while ($str) {
+ $ptr = strlen($str);
+ if ($ptr > $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 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;
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mime_Decode
+{
+ public static function splitMime($body, $boundary)
+ {
+ // TODO: we're ignoring \r for now - is this function fast enough and is it safe to asume noone needs \r?
+ $body = str_replace("\r", '', $body);
+
+ $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_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 splitMessageStruct($message, $boundary, $EOL = IPF_Mime::LINEEND)
+ {
+ $parts = self::splitMime($message, $boundary);
+ if (count($parts) <= 0) {
+ return null;
+ }
+ $result = array();
+ foreach ($parts as $part) {
+ self::splitMessage($part, $headers, $body, $EOL);
+ $result[] = array('header' => $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_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);
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mime_Message
+{
+
+ protected $_parts = array();
+ protected $_mime = null;
+
+ public function getParts()
+ {
+ return $this->_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_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_Exception('Unknown header ignored for MimePart:' . $key);
+ }
+ }
+ $res->addPart($newPart);
+ }
+ return $res;
+ }
+}
--- /dev/null
+<?php
+
+class IPF_Mime_Part {
+
+ public $type = IPF_Mime::TYPE_OCTETSTREAM;
+ public $encoding = IPF_Mime::ENCODING_8BIT;
+ public $id;
+ public $disposition;
+ public $filename;
+ public $description;
+ public $charset;
+ public $boundary;
+ protected $_content;
+ protected $_isStream = false;
+
+
+ public function __construct($content)
+ {
+ $this->_content = $content;
+ if (is_resource($content)) {
+ $this->_isStream = true;
+ }
+ }
+
+ public function isStream()
+ {
+ return $this->_isStream;
+ }
+
+ public function getEncodedStream()
+ {
+ if (!$this->_isStream) {
+ throw new IPF_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' => 76,
+ 'line-break-chars' => IPF_Mime::LINEEND
+ )
+ );
+ if (!is_resource($filter)) {
+ throw new IPF_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' => 76,
+ 'line-break-chars' => IPF_Mime::LINEEND
+ )
+ );
+ if (!is_resource($filter)) {
+ throw new IPF_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;
+ }
+}