]> git.andy128k.dev Git - ipf-mail.git/commitdiff
place code into subfolder
authorAndrey Kutejko <andy128k@gmail.com>
Tue, 4 Feb 2014 19:39:31 +0000 (21:39 +0200)
committerAndrey Kutejko <andy128k@gmail.com>
Tue, 4 Feb 2014 19:39:31 +0000 (21:39 +0200)
41 files changed:
composer.json
exception.php [deleted file]
mail.php [deleted file]
mail/exception.php [deleted file]
mail/message.php [deleted file]
mail/part.php [deleted file]
mail/protocol/abstract.php [deleted file]
mail/protocol/imap.php [deleted file]
mail/protocol/pop3.php [deleted file]
mail/protocol/smtp.php [deleted file]
mail/protocol/smtp/auth/crammd5.php [deleted file]
mail/protocol/smtp/auth/login.php [deleted file]
mail/protocol/smtp/auth/plain.php [deleted file]
mail/transport/abstract.php [deleted file]
mail/transport/sendmail.php [deleted file]
mail/transport/smtp.php [deleted file]
mime.php [deleted file]
mime/decode.php [deleted file]
mime/exception.php [deleted file]
mime/message.php [deleted file]
mime/part.php [deleted file]
src/exception.php [new file with mode: 0644]
src/mail.php [new file with mode: 0644]
src/mail/exception.php [new file with mode: 0644]
src/mail/message.php [new file with mode: 0644]
src/mail/part.php [new file with mode: 0644]
src/mail/protocol/abstract.php [new file with mode: 0644]
src/mail/protocol/imap.php [new file with mode: 0644]
src/mail/protocol/pop3.php [new file with mode: 0644]
src/mail/protocol/smtp.php [new file with mode: 0644]
src/mail/protocol/smtp/auth/crammd5.php [new file with mode: 0644]
src/mail/protocol/smtp/auth/login.php [new file with mode: 0644]
src/mail/protocol/smtp/auth/plain.php [new file with mode: 0644]
src/mail/transport/abstract.php [new file with mode: 0644]
src/mail/transport/sendmail.php [new file with mode: 0644]
src/mail/transport/smtp.php [new file with mode: 0644]
src/mime.php [new file with mode: 0644]
src/mime/decode.php [new file with mode: 0644]
src/mime/exception.php [new file with mode: 0644]
src/mime/message.php [new file with mode: 0644]
src/mime/part.php [new file with mode: 0644]

index 6d743c987b3ce0e36e4cc28c87b0574b61caaeaa..71bb09772f442ca8e73fb27ff8df2d7a2d1bc197 100644 (file)
@@ -16,7 +16,7 @@
     "phpunit/phpunit": "3.7.*"
   },
   "autoload": {
-    "classmap" : ["."]
+    "classmap" : ["src/"]
   }
 }
 
diff --git a/exception.php b/exception.php
deleted file mode 100644 (file)
index ef36fc7..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class IPF_Exception extends Exception
-{
-}
-
diff --git a/mail.php b/mail.php
deleted file mode 100644 (file)
index 7050442..0000000
--- a/mail.php
+++ /dev/null
@@ -1,346 +0,0 @@
-<?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='utf-8')
-    {
-        $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_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 (file)
index 8ec3e7a..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class IPF_Mail_Exception extends Exception
-{
-}
-
diff --git a/mail/message.php b/mail/message.php
deleted file mode 100644 (file)
index 5220def..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-<?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_Mail_Exception('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;
-    }
-}
diff --git a/mail/part.php b/mail/part.php
deleted file mode 100644 (file)
index f74df14..0000000
+++ /dev/null
@@ -1,238 +0,0 @@
-<?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_Mail_Exception('handler is not a valid mail handler');
-            }
-            if (!isset($params['id'])) {
-                throw new IPF_Mail_Exception('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_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 (file)
index 88eb17a..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-<?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_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 (file)
index 2e05564..0000000
+++ /dev/null
@@ -1,505 +0,0 @@
-<?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_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}<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_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 (file)
index cd148c9..0000000
+++ /dev/null
@@ -1,246 +0,0 @@
-<?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_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 (file)
index 008f26f..0000000
+++ /dev/null
@@ -1,199 +0,0 @@
-<?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_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 (file)
index 576033e..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-<?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;
-    }
-}
diff --git a/mail/protocol/smtp/auth/login.php b/mail/protocol/smtp/auth/login.php
deleted file mode 100644 (file)
index 4d24b9f..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<?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;
-    }
-}
diff --git a/mail/protocol/smtp/auth/plain.php b/mail/protocol/smtp/auth/plain.php
deleted file mode 100644 (file)
index 38ed235..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<?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;
-    }
-}
diff --git a/mail/transport/abstract.php b/mail/transport/abstract.php
deleted file mode 100644 (file)
index ea98e19..0000000
+++ /dev/null
@@ -1,179 +0,0 @@
-<?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 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 (file)
index 6a16fc1..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-<?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_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 (file)
index e099a4d..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-<?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_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 (file)
index f303360..0000000
--- a/mime.php
+++ /dev/null
@@ -1,205 +0,0 @@
-<?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 = 72;
-    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 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 (file)
index e4a5d42..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-<?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_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 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_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 (file)
index 54d46df..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class IPF_Mime_Exception extends Exception
-{
-}
-
diff --git a/mime/message.php b/mime/message.php
deleted file mode 100644 (file)
index b06848f..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-<?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_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 (file)
index 1fbc583..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-<?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_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 (file)
index 0000000..ef36fc7
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+
+class IPF_Exception extends Exception
+{
+}
+
diff --git a/src/mail.php b/src/mail.php
new file mode 100644 (file)
index 0000000..7050442
--- /dev/null
@@ -0,0 +1,346 @@
+<?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='utf-8')
+    {
+        $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_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 (file)
index 0000000..8ec3e7a
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+
+class IPF_Mail_Exception extends Exception
+{
+}
+
diff --git a/src/mail/message.php b/src/mail/message.php
new file mode 100644 (file)
index 0000000..5220def
--- /dev/null
@@ -0,0 +1,42 @@
+<?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_Mail_Exception('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;
+    }
+}
diff --git a/src/mail/part.php b/src/mail/part.php
new file mode 100644 (file)
index 0000000..f74df14
--- /dev/null
@@ -0,0 +1,238 @@
+<?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_Mail_Exception('handler is not a valid mail handler');
+            }
+            if (!isset($params['id'])) {
+                throw new IPF_Mail_Exception('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_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 (file)
index 0000000..88eb17a
--- /dev/null
@@ -0,0 +1,147 @@
+<?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_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 (file)
index 0000000..2e05564
--- /dev/null
@@ -0,0 +1,505 @@
+<?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_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}<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_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 (file)
index 0000000..cd148c9
--- /dev/null
@@ -0,0 +1,246 @@
+<?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_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 (file)
index 0000000..008f26f
--- /dev/null
@@ -0,0 +1,199 @@
+<?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_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 (file)
index 0000000..576033e
--- /dev/null
@@ -0,0 +1,49 @@
+<?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;
+    }
+}
diff --git a/src/mail/protocol/smtp/auth/login.php b/src/mail/protocol/smtp/auth/login.php
new file mode 100644 (file)
index 0000000..4d24b9f
--- /dev/null
@@ -0,0 +1,35 @@
+<?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;
+    }
+}
diff --git a/src/mail/protocol/smtp/auth/plain.php b/src/mail/protocol/smtp/auth/plain.php
new file mode 100644 (file)
index 0000000..38ed235
--- /dev/null
@@ -0,0 +1,33 @@
+<?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;
+    }
+}
diff --git a/src/mail/transport/abstract.php b/src/mail/transport/abstract.php
new file mode 100644 (file)
index 0000000..ea98e19
--- /dev/null
@@ -0,0 +1,179 @@
+<?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 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 (file)
index 0000000..6a16fc1
--- /dev/null
@@ -0,0 +1,71 @@
+<?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_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 (file)
index 0000000..e099a4d
--- /dev/null
@@ -0,0 +1,91 @@
+<?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_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 (file)
index 0000000..f303360
--- /dev/null
@@ -0,0 +1,205 @@
+<?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 = 72;
+    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 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 (file)
index 0000000..e4a5d42
--- /dev/null
@@ -0,0 +1,153 @@
+<?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_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 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_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 (file)
index 0000000..54d46df
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+
+class IPF_Mime_Exception extends Exception
+{
+}
+
diff --git a/src/mime/message.php b/src/mime/message.php
new file mode 100644 (file)
index 0000000..b06848f
--- /dev/null
@@ -0,0 +1,155 @@
+<?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_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 (file)
index 0000000..1fbc583
--- /dev/null
@@ -0,0 +1,128 @@
+<?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_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;
+    }
+}