lib/kolab lib/kolab_sync_backend.php lib/kolab_sync_data_email.php lib/kolab_sync_message.php tests/message.php tests/phpunit.xml tests/src

Aleksander Machniak machniak at kolabsys.com
Thu Oct 11 15:29:55 CEST 2012


 lib/kolab/kolab_storage.php   |    2 
 lib/kolab_sync_backend.php    |  275 -------------------------
 lib/kolab_sync_data_email.php |   69 +++---
 lib/kolab_sync_message.php    |  448 ++++++++++++++++++++++++++++++++++++++++++
 tests/message.php             |   99 +++++++++
 tests/phpunit.xml             |    1 
 tests/src/mail.alternative    |   27 ++
 tests/src/mail.alternative2   |   30 ++
 tests/src/mail.mixed          |   24 ++
 tests/src/mail.plain          |   10 
 tests/src/mail.plain.append   |   10 
 tests/src/mail.plain.mixed    |   19 +
 12 files changed, 704 insertions(+), 310 deletions(-)

New commits:
commit 58a747312a76add8a4e5860158774be2f4d5608b
Author: Aleksander Machniak <alec at alec.pl>
Date:   Thu Oct 11 15:02:43 2012 +0200

    Improved email messages handling, includes fixed fowarding

diff --git a/lib/kolab/kolab_storage.php b/lib/kolab/kolab_storage.php
index e14156d..1292b8b 100644
--- a/lib/kolab/kolab_storage.php
+++ b/lib/kolab/kolab_storage.php
@@ -608,6 +608,8 @@ class kolab_storage
      */
     static function folder_type($folder)
     {
+        self::setup();
+
         $metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
 
         if (!is_array($metadata)) {
diff --git a/lib/kolab_sync_backend.php b/lib/kolab_sync_backend.php
index 522e1cf..dd29629 100644
--- a/lib/kolab_sync_backend.php
+++ b/lib/kolab_sync_backend.php
@@ -730,279 +730,4 @@ class kolab_sync_backend
 
         return false;
     }
-
-    /**
-     * Send the given message using the configured method.
-     *
-     * @param string $message    Complete message source
-     * @param array  $smtp_error SMTP error array (reference)
-     * @param array  $smtp_opts  SMTP options (e.g. DSN request)
-     *
-     * @return boolean Send status.
-     */
-    public function send_message(&$message, &$smtp_error, $smtp_opts = null)
-    {
-        $rcube = rcube::get_instance();
-
-        list($headers, $message) = $this->parse_mime($message);
-
-        $mailto = $headers['To'];
-
-        $headers['User-Agent'] .= sprintf('%s v.%.1f', $rcube->app_name, kolab_sync::VERSION);
-        if ($agent = $rcube->config->get('useragent')) {
-            $headers['User-Agent'] .= '/' . $agent;
-        }
-
-        if (empty($headers['From'])) {
-            $headers['From'] = $this->get_identity();
-        }
-        if (empty($headers['Message-ID'])) {
-            $headers['Message-ID'] = $this->gen_message_id();
-        }
-
-        // remove empty headers
-        $headers = array_filter($headers);
-
-        // send thru SMTP server using custom SMTP library
-        if ($rcube->config->get('smtp_server')) {
-            $smtp_headers = $headers;
-            // generate list of recipients
-            $recipients = array();
-
-            if (!empty($headers['To']))
-                $recipients[] = $headers['To'];
-            if (!empty($headers['Cc']))
-                $recipients[] = $headers['Cc'];
-            if (!empty($headers['Bcc']))
-                $recipients[] = $headers['Bcc'];
-
-            // remove Bcc header
-            unset($smtp_headers['Bcc']);
-
-            // send message
-            if (!is_object($rcube->smtp)) {
-                $rcube->smtp_init(true);
-            }
-
-            $sent = $rcube->smtp->send_mail($headers['From'], $recipients, $smtp_headers, $message, $smtp_opts);
-            $smtp_response = $rcube->smtp->get_response();
-            $smtp_error    = $rcube->smtp->get_error();
-
-            // log error
-            if (!$sent) {
-                rcube::raise_error(array('code' => 800, 'type' => 'smtp',
-                    'line' => __LINE__, 'file' => __FILE__,
-                    'message' => "SMTP error: ".join("\n", $smtp_response)), true, false);
-            }
-        }
-        // send mail using PHP's mail() function
-        else {
-            $mail_headers = $headers;
-            $delim        = $rcube->config->header_delimiter();
-            $subject      = $headers['Subject'];
-            $to           = $headers['To'];
-
-            // unset some headers because they will be added by the mail() function
-            unset($mail_headers['To'], $mail_headers['Subject']);
-
-            // #1485779
-            if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
-                if (preg_match_all('/<([^@]+@[^>]+)>/', $to, $m)) {
-                    $to = implode(', ', $m[1]);
-                }
-            }
-
-            foreach ($mail_headers as $header => $header_value) {
-                $mail_headers[$header] = $header . ': ' . $header_value;
-            }
-            $header_str = rtrim(implode("\r\n", $mail_headers));
-
-            if ($delim != "\r\n") {
-                $header_str = str_replace("\r\n", $delim, $header_str);
-                $msg_body   = str_replace("\r\n", $delim, $message);
-                $to         = str_replace("\r\n", $delim, $to);
-                $subject    = str_replace("\r\n", $delim, $subject);
-            }
-
-            if (ini_get('safe_mode'))
-                $sent = mail($to, $subject, $message, $header_str);
-            else
-                $sent = mail($to, $subject, $message, $header_str, "-f$from");
-        }
-
-        if ($sent) {
-            $rcube->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $message));
-
-            // remove MDN headers after sending
-            unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
-
-            // get all recipients
-            if ($headers['Cc'])
-                $mailto .= ' ' . $headers['Cc'];
-            if ($headers['Bcc'])
-                $mailto .= ' ' . $headers['Bcc'];
-            if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
-                $mailto = implode(', ', array_unique($m[1]));
-
-            if ($rcube->config->get('smtp_log')) {
-                rcube::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
-                    $rcube->get_user_name(),
-                    $_SERVER['REMOTE_ADDR'],
-                    $mailto,
-                    !empty($smtp_response) ? join('; ', $smtp_response) : ''));
-            }
-        }
-
-        unset($headers['Bcc']);
-
-        // Build the message back
-        foreach ($headers as $header => $header_value) {
-            $headers[$header] = $header . ': ' . $header_value;
-        }
-        $message = trim(implode("\r\n", $headers)) . "\r\n\r\n" . ltrim($message);
-
-        return $sent;
-    }
-
-    /**
-     * MIME message parser
-     *
-     * @param string|resource $message     MIME message source
-     * @param bool            $decode_body Enables body decoding
-     *
-     * @return array Message headers array and message body
-     */
-    public function parse_mime($message, $decode_body = false)
-    {
-        if (is_resource($message)) {
-            $message = stream_get_contents($message);
-        }
-
-        list($headers, $message) = preg_split('/\r?\n\r?\n/', $message, 2, PREG_SPLIT_NO_EMPTY);
-
-        // Parse headers to get sender and recipients
-        $headers = str_replace("\r\n", "\n", $headers);
-        $headers = explode("\n", trim($headers));
-
-        $ln    = 0;
-        $lines = array();
-
-        foreach ($headers as $line) {
-            if (ord($line[0]) <= 32) {
-                $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . $line;
-            }
-            else {
-                $lines[++$ln] = trim($line);
-            }
-        }
-
-        $headers     = array();
-        $headers_map = array(
-            'subject' => 'Subject',
-            'from'    => 'From',
-            'to'      => 'To',
-            'cc'      => 'Cc',
-            'bcc'     => 'Bcc',
-            'message-id'   => 'Message-ID',
-            'references'   => 'References',
-            'content-type' => 'Content-Type',
-            'content-transfer-encoding' => 'Content-Transfer-Encoding',
-        );
-
-        foreach ($lines as $line) {
-            list($field, $string) = explode(':', $line, 2);
-            $_field = strtolower($field);
-
-            if (isset($headers_map[$_field])) {
-                $field = $headers_map[$_field];
-            }
-
-            $headers[$field] = trim($string);
-        }
-
-        // Decode body
-        if ($decode_body) {
-            $message  = str_replace("\r\n", "\n", $message);
-            $encoding = strtolower($headers['Content-Transfer-Encoding']);
-
-            switch ($encoding) {
-            case 'base64':
-                $message = base64_decode($message);
-                break;
-            case 'quoted-printable':
-                $message = quoted_printable_decode($message);
-                break;
-            }
-        }
-
-        return array($headers, $message);
-    }
-
-    /**
-     * Creates complete MIME message body
-     *
-     * @param array  $headers Message headers
-     * @param string $body    Message body
-     *
-     * @return string Message source
-     */
-    public function build_mime($headers, $body)
-    {
-        // Encode the body
-        $encoding = strtolower($headers['Content-Transfer-Encoding']);
-
-        switch ($encoding) {
-        case 'base64':
-            $body = base64_encode($body);
-            $body = chunk_split($body, 76, "\r\n");
-            break;
-        case 'quoted-printable':
-            $body = quoted_printable_encode($body);
-            break;
-        }
-
-        foreach ($headers as $header => $header_value) {
-            $headers[$header] = $header . ': ' . $header_value;
-        }
-
-        // Build the complete message
-        return trim(implode("\r\n", $headers)) . "\r\n\r\n" . ltrim($body);
-    }
-
-    /**
-     * Returns email address string from default identity of the current user
-     */
-    protected function get_identity()
-    {
-        $user = kolab_sync::get_instance()->user;
-
-        if ($identity = $user->get_identity()) {
-            return format_email_recipient(format_email($identity['email']), $identity['name']);
-        }
-    }
-
-    /**
-     * Unique Message-ID generator.
-     *
-     * @return string Message-ID
-     */
-    protected function gen_message_id()
-    {
-        $user        = kolab_sync::get_instance()->user;
-        $local_part  = md5(uniqid('rcmail'.mt_rand(),true));
-        $domain_part = $user->get_username('domain');
-
-        // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
-        if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
-            foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
-                $host = preg_replace('/:[0-9]+$/', '', $host);
-                if ($host && preg_match('/\.[a-z]+$/i', $host)) {
-                    $domain_part = $host;
-                }
-            }
-        }
-
-        return sprintf('<%s@%s>', $local_part, $domain_part);
-    }
-
 }
diff --git a/lib/kolab_sync_data_email.php b/lib/kolab_sync_data_email.php
index e197b23..1f6aed4 100644
--- a/lib/kolab_sync_data_email.php
+++ b/lib/kolab_sync_data_email.php
@@ -597,14 +597,18 @@ class kolab_sync_data_email extends kolab_sync_data implements Syncroton_Data_ID
     /**
      * Send an email
      *
-     * @param resource|string $body       MIME message
-     * @param boolean         $saveInSent Enables saving the sent message in Sent folder
+     * @param mixed   $message    MIME message
+     * @param boolean $saveInSent Enables saving the sent message in Sent folder
      *
      * @param throws Syncroton_Exception_Status
      */
-    public function sendEmail($body, $saveInSent)
+    public function sendEmail($message, $saveInSent)
     {
-        $sent = $this->backend->send_message($body, $smtp_error);
+        if (!($message instanceof kolab_sync_message)) {
+            $message = new kolab_sync_message($message);
+        }
+
+        $sent = $message->send($smtp_error);
 
         if (!$sent) {
             throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAIL_SUBMISSION_FAILED);
@@ -615,7 +619,7 @@ class kolab_sync_data_email extends kolab_sync_data implements Syncroton_Data_ID
             $sent_folder = kolab_sync::get_instance()->config->get('sent_mbox');
 
             if (strlen($sent_folder) && $this->storage->folder_exists($sent_folder)) {
-                return $this->storage->save_message($sent_folder, $body);
+                return $this->storage->save_message($sent_folder, $message->source());
             }
         }
     }
@@ -646,35 +650,34 @@ class kolab_sync_data_email extends kolab_sync_data implements Syncroton_Data_ID
         or a meeting, the behavior of the SmartForward command is the same as that of the SmartReply command (section 2.2.2.18).
         */
 
-        $msg     = $this->parseMessageId($itemId);
-        $message = $this->getObject($itemId);
+        $msg = $this->parseMessageId($itemId);
 
-        if (!$message) {
+        if (empty($msg)) {
             throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND);
         }
 
         // Parse message
-        list($headers, $body) = $this->backend->parse_mime($body, true);
+        $sync_msg = new kolab_sync_message($body);
 
-        // Get original message body
+        // forward original message as attachment
         if (!$replaceMime) {
-            // @TODO: here we're assuming that reply message is in text/plain format
-            // So, original message will be converted to plain text if needed
-            // @TODO: what about forward-as-attachment?
-            $message_body = $this->getMessageBody($message, false);
-            $message_body = trim($message_body);
+            $this->storage->set_folder($msg['foldername']);
+            $attachment = $this->storage->get_raw_body($msg['uid']);
 
-            // Join bodies
-            $body = rtrim($body) . "\n\n" . ltrim($message_body);
+            if (empty($attachment)) {
+                throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND);
+            }
 
-            // @TODO: add attachments from the original message
+            $sync_msg->add_attachment($attachment, array(
+                'encoding'     => '8bit',
+                'content_type' => 'message/rfc822',
+                'disposition'  => 'inline',
+                //'name'         => 'message.eml',
+            ));
         }
 
-        // Create complete message source
-        $body = $this->backend->build_mime($headers, $body);
-
         // Send message
-        $sent = $this->sendEmail($body, $saveInSent);
+        $sent = $this->sendEmail($sync_msg, $saveInSent);
 
         // Set FORWARDED flag on the replied message
         if (empty($message->headers->flags['FORWARDED'])) {
@@ -702,9 +705,13 @@ class kolab_sync_data_email extends kolab_sync_data implements Syncroton_Data_ID
             throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND);
         }
 
-        // Parse message
-        // @TODO: messages with attachments
-        list($headers, $body) = $this->backend->parse_mime($body, true);
+        $sync_msg = new kolab_sync_message($body);
+        $headers = $sync_msg->headers();
+
+        // Add References header
+        if (empty($headers['References'])) {
+            $sync_msg->set_header('References', trim($message->headers->references . ' ' . $message->headers->messageID));
+        }
 
         // Get original message body
         if (!$replaceMime) {
@@ -716,19 +723,11 @@ class kolab_sync_data_email extends kolab_sync_data implements Syncroton_Data_ID
             $message_body = self::wrap_and_quote(trim($message_body), 72);
 
             // Join bodies
-            $body = rtrim($body) . "\n" . ltrim($message_body);
-        }
-
-        // Add References header
-        if (empty($headers['References'])) {
-            $headers['References'] = trim($message->headers->references . ' ' . $message->headers->messageID);
+            $sync_msg->append("\n" . ltrim($message_body));
         }
 
-        // Create complete message source
-        $body = $this->backend->build_mime($headers, $body);
-
         // Send message
-        $sent = $this->sendEmail($body, $saveInSent);
+        $sent = $this->sendEmail($sync_msg, $saveInSent);
 
         // Set ANSWERED flag on the replied message
         if (empty($message->headers->flags['ANSWERED'])) {
diff --git a/lib/kolab_sync_message.php b/lib/kolab_sync_message.php
new file mode 100644
index 0000000..a31a6c1
--- /dev/null
+++ b/lib/kolab_sync_message.php
@@ -0,0 +1,448 @@
+<?php
+
+/**
+ +--------------------------------------------------------------------------+
+ | Kolab Sync (ActiveSync for Kolab)                                        |
+ |                                                                          |
+ | Copyright (C) 2011-2012, Kolab Systems AG <contact at kolabsys.com>         |
+ |                                                                          |
+ | This program is free software: you can redistribute it and/or modify     |
+ | it under the terms of the GNU Affero General Public License as published |
+ | by the Free Software Foundation, either version 3 of the License, or     |
+ | (at your option) any later version.                                      |
+ |                                                                          |
+ | This program is distributed in the hope that it will be useful,          |
+ | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
+ | GNU Affero General Public License for more details.                      |
+ |                                                                          |
+ | You should have received a copy of the GNU Affero General Public License |
+ | along with this program. If not, see <http://www.gnu.org/licenses/>      |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak at kolabsys.com>                      |
+ +--------------------------------------------------------------------------+
+*/
+
+class kolab_sync_message
+{
+    protected $headers = array();
+    protected $body;
+    protected $ctype;
+    protected $ctype_params = array();
+
+    /**
+     * Constructor
+     *
+     * @param string|resource $source MIME message source
+     */
+    function __construct($source)
+    {
+        $this->parse_mime($source);
+    }
+
+    /**
+     * Returns message headers
+     *
+     * @return array Message headers
+     */
+    public function headers()
+    {
+        return $this->headers;
+    }
+
+    public function source()
+    {
+        $headers = array();
+
+        // Build the message back
+        foreach ($this->headers as $header => $header_value) {
+            $headers[$header] = $header . ': ' . $header_value;
+        }
+
+        return trim(implode("\r\n", $headers)) . "\r\n\r\n" . ltrim($this->body);
+        // @TODO: work with file streams
+    }
+
+    /**
+     * Appends text at the end of the message body
+     *
+     * @todo: HTML support
+     *
+     * @param string $text    Text to append
+     * @param string $charset Text charset
+     */
+    public function append($text, $charset = null)
+    {
+        if ($this->ctype == 'text/plain') {
+            // decode body
+            $body = $this->decode($this->body, $this->headers['Content-Transfer-Encoding']);
+            $body = rcube_charset::convert($body, $this->ctype_params['charset'], $charset);
+            // append text
+            $body .= $text;
+            // encode and save
+            $body = rcube_charset::convert($body, $charset, $this->ctype_params['charset']);
+            $this->body = $this->encode($body, $this->headers['Content-Transfer-Encoding']);
+        }
+    }
+
+    /**
+     * Adds attachment to the message
+     *
+     * @param string $body   Attachment body (not encoded)
+     * @param string $params Attachment parameters (Mail_mimePart format)
+     */
+    public function add_attachment($body, $params = array())
+    {
+        // convert the message into multipart/mixed
+        if ($this->ctype != 'multipart/mixed') {
+            $boundary = '_' . md5(rand() . microtime());
+
+            $this->body = "--$boundary\r\n"
+                ."Content-Type: " . $this->headers['Content-Type']."\r\n"
+                ."Content-Transfer-Encoding: " . $this->headers['Content-Transfer-Encoding']."\r\n"
+                ."\r\n" . trim($this->body) . "\r\n"
+                ."--$boundary\r\n";
+
+            $this->ctype = 'multipart/mixed';
+            $this->ctype_params = array('boundary' => $boundary);
+            unset($this->headers['Content-Transfer-Encoding']);
+            $this->save_content_type($this->ctype, $this->ctype_params);
+        }
+
+        // make sure MIME-Version header is set, it's required by some servers
+        if (empty($this->headers['MIME-Version'])) {
+            $this->headers['MIME-Version'] = '1.0';
+        }
+
+        $boundary = $this->ctype_params['boundary'];
+
+        $part = new Mail_mimePart($body, $params);
+        $body = $part->encode();
+
+        foreach ($body['headers'] as $name => $value) {
+            $body['headers'][$name] = $name . ': ' . $value;
+        }
+
+        // add the attachment to the end of the message
+        $this->body = rtrim($this->body) . "\r\n"
+            .implode("\r\n", $body['headers']) . "\r\n\r\n"
+            .$body['body'] . "\r\n--$boundary\r\n";
+    }
+
+    /**
+     * Sets the value of specified message header
+     *
+     * @param string $name  Header name
+     * @param string $value Header value
+     */
+    public function set_header($name, $value)
+    {
+        $name = $this->normalize_header_name($name);
+
+        if ($name != 'Content-Type') {
+            $this->headers[$name] = $value;
+        }
+    }
+
+    /**
+     * Send the given message using the configured method.
+     *
+     * @param array $smtp_error SMTP error array (reference)
+     * @param array $smtp_opts  SMTP options (e.g. DSN request)
+     *
+     * @return boolean Send status.
+     */
+    public function send(&$smtp_error = null, $smtp_opts = null)
+    {
+        $rcube   = rcube::get_instance();
+        $headers = $this->headers;
+
+        $mailto = $headers['To'];
+
+        $headers['User-Agent'] .= sprintf('%s v.%.1f', $rcube->app_name, kolab_sync::VERSION);
+        if ($agent = $rcube->config->get('useragent')) {
+            $headers['User-Agent'] .= '/' . $agent;
+        }
+
+        if (empty($headers['From'])) {
+            $headers['From'] = $this->get_identity();
+        }
+        if (empty($headers['Message-ID'])) {
+            $headers['Message-ID'] = $this->gen_message_id();
+        }
+
+        // remove empty headers
+        $headers = array_filter($headers);
+
+        // send thru SMTP server using custom SMTP library
+        if ($rcube->config->get('smtp_server')) {
+            $smtp_headers = $headers;
+            // generate list of recipients
+            $recipients = array();
+
+            if (!empty($headers['To']))
+                $recipients[] = $headers['To'];
+            if (!empty($headers['Cc']))
+                $recipients[] = $headers['Cc'];
+            if (!empty($headers['Bcc']))
+                $recipients[] = $headers['Bcc'];
+
+            // remove Bcc header
+            unset($smtp_headers['Bcc']);
+
+            // send message
+            if (!is_object($rcube->smtp)) {
+                $rcube->smtp_init(true);
+            }
+
+            $sent = $rcube->smtp->send_mail($headers['From'], $recipients, $smtp_headers, $this->body, $smtp_opts);
+            $smtp_response = $rcube->smtp->get_response();
+            $smtp_error    = $rcube->smtp->get_error();
+
+            // log error
+            if (!$sent) {
+                rcube::raise_error(array('code' => 800, 'type' => 'smtp',
+                    'line' => __LINE__, 'file' => __FILE__,
+                    'message' => "SMTP error: ".join("\n", $smtp_response)), true, false);
+            }
+        }
+        // send mail using PHP's mail() function
+        else {
+            $mail_headers = $headers;
+            $delim        = $rcube->config->header_delimiter();
+            $subject      = $headers['Subject'];
+            $to           = $headers['To'];
+
+            // unset some headers because they will be added by the mail() function
+            unset($mail_headers['To'], $mail_headers['Subject']);
+
+            // #1485779
+            if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+                if (preg_match_all('/<([^@]+@[^>]+)>/', $to, $m)) {
+                    $to = implode(', ', $m[1]);
+                }
+            }
+
+            foreach ($mail_headers as $header => $header_value) {
+                $mail_headers[$header] = $header . ': ' . $header_value;
+            }
+            $header_str = rtrim(implode("\r\n", $mail_headers));
+
+            if ($delim != "\r\n") {
+                $header_str = str_replace("\r\n", $delim, $header_str);
+                $msg_body   = str_replace("\r\n", $delim, $this->body);
+                $to         = str_replace("\r\n", $delim, $to);
+                $subject    = str_replace("\r\n", $delim, $subject);
+            }
+
+            if (ini_get('safe_mode')) {
+                $sent = mail($to, $subject, $msg_body, $header_str);
+            }
+            else {
+                $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
+            }
+        }
+
+        if ($sent) {
+            $rcube->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $this->body));
+
+            // remove MDN headers after sending
+            unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
+
+            // get all recipients
+            if ($headers['Cc'])
+                $mailto .= ' ' . $headers['Cc'];
+            if ($headers['Bcc'])
+                $mailto .= ' ' . $headers['Bcc'];
+            if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
+                $mailto = implode(', ', array_unique($m[1]));
+
+            if ($rcube->config->get('smtp_log')) {
+                rcube::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
+                    $rcube->get_user_name(),
+                    $_SERVER['REMOTE_ADDR'],
+                    $mailto,
+                    !empty($smtp_response) ? join('; ', $smtp_response) : ''));
+            }
+        }
+
+        unset($headers['Bcc']);
+
+        $this->headers = $headers;
+
+        return $sent;
+    }
+    /**
+     * MIME message parser
+     *
+     * @param string|resource $message     MIME message source
+     * @param bool            $decode_body Enables body decoding
+     *
+     * @return array Message headers array and message body
+     */
+    protected function parse_mime($message)
+    {
+        // @TODO: work with stream, to workaround memory issues with big messages
+        if (is_resource($message)) {
+            $message = stream_get_contents($message);
+        }
+
+        list($headers, $message) = preg_split('/\r?\n\r?\n/', $message, 2, PREG_SPLIT_NO_EMPTY);
+
+        // Parse headers
+        $headers = str_replace("\r\n", "\n", $headers);
+        $headers = explode("\n", trim($headers));
+
+        $ln    = 0;
+        $lines = array();
+
+        foreach ($headers as $line) {
+            if (ord($line[0]) <= 32) {
+                $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . $line;
+            }
+            else {
+                $lines[++$ln] = trim($line);
+            }
+        }
+
+        // Unify char-case of header names
+        $headers = array();
+        foreach ($lines as $line) {
+            list($field, $string) = explode(':', $line, 2);
+            $field = $this->normalize_header_name($field);
+            $headers[$field] = trim($string);
+        }
+
+        // parse Content-Type header
+        $ctype_parts = preg_split('/[; ]+/', $headers['Content-Type']);
+        $this->ctype = strtolower(array_shift($ctype_parts));
+        foreach ($ctype_parts as $part) {
+            if (preg_match('/^([a-z-_]+)\s*=\s*(.+)$/i', trim($part), $m)) {
+                $this->ctype_params[strtolower($m[1])] = trim($m[2], '"');
+            }
+        }
+
+        if (!empty($headers['Content-Transfer-Encoding'])) {
+            $headers['Content-Transfer-Encoding'] = strtolower($headers['Content-Transfer-Encoding']);
+        }
+
+        $this->headers = $headers;
+        $this->body    = $message;
+    }
+
+    protected function normalize_header_name($name)
+    {
+        $headers_map = array(
+            'subject' => 'Subject',
+            'from'    => 'From',
+            'to'      => 'To',
+            'cc'      => 'Cc',
+            'bcc'     => 'Bcc',
+            'message-id'   => 'Message-ID',
+            'references'   => 'References',
+            'content-type' => 'Content-Type',
+            'content-transfer-encoding' => 'Content-Transfer-Encoding',
+        );
+
+        $name_lc = strtolower($name);
+
+        return isset($headers_map[$name_lc]) ? $headers_map[$name_lc] : $name;
+    }
+
+    /**
+     * Encodes message/part body
+     *
+     * @param string $body     Message/part body
+     * @param string $encoding Content encoding
+     *
+     * @return string Encoded body
+     */
+    protected function encode($body, $encoding)
+    {
+        switch ($encoding) {
+        case 'base64':
+            $body = base64_encode($body);
+            $body = chunk_split($body, 76, "\r\n");
+            break;
+        case 'quoted-printable':
+            $body = quoted_printable_encode($body);
+            break;
+        }
+
+        return $body;
+    }
+
+    /**
+     * Decodes message/part body
+     *
+     * @param string $body     Message/part body
+     * @param string $encoding Content encoding
+     *
+     * @return string Decoded body
+     */
+    protected function decode($body, $encoding)
+    {
+        $body  = str_replace("\r\n", "\n", $body);
+
+        switch ($encoding) {
+        case 'base64':
+            $body = base64_decode($body);
+            break;
+        case 'quoted-printable':
+            $body = quoted_printable_decode($body);
+            break;
+        }
+
+        return $body;
+    }
+
+    /**
+     * Returns email address string from default identity of the current user
+     */
+    protected function get_identity()
+    {
+        $user = kolab_sync::get_instance()->user;
+
+        if ($identity = $user->get_identity()) {
+            return format_email_recipient(format_email($identity['email']), $identity['name']);
+        }
+    }
+
+    /**
+     * Unique Message-ID generator.
+     *
+     * @return string Message-ID
+     */
+    protected function gen_message_id()
+    {
+        $user        = kolab_sync::get_instance()->user;
+        $local_part  = md5(uniqid('rcmail'.mt_rand(),true));
+        $domain_part = $user->get_username('domain');
+
+        // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
+        if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
+            foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
+                $host = preg_replace('/:[0-9]+$/', '', $host);
+                if ($host && preg_match('/\.[a-z]+$/i', $host)) {
+                    $domain_part = $host;
+                }
+            }
+        }
+
+        return sprintf('<%s@%s>', $local_part, $domain_part);
+    }
+
+    protected function save_content_type($ctype, $params = array())
+    {
+        $this->ctype        = $ctype;
+        $this->ctype_params = $params;
+
+        $this->headers['Content-Type'] = $ctype;
+        if (!empty($params)) {
+            foreach ($params as $name => $value) {
+                $this->headers['Content-Type'] .= sprintf('; %s="%s"', $name, $value);
+            }
+        }
+    }
+
+}
diff --git a/tests/message.php b/tests/message.php
new file mode 100644
index 0000000..1cafa5f
--- /dev/null
+++ b/tests/message.php
@@ -0,0 +1,99 @@
+<?php
+
+class message extends PHPUnit_Framework_TestCase
+{
+    function setUp()
+    {
+    }
+
+
+    /**
+     * Test message parsing and headers setting
+     */
+    function test_headers()
+    {
+        $source  = file_get_contents(TESTS_DIR . '/src/mail.plain');
+        $message = new kolab_sync_message($source);
+        $headers = $message->headers();
+
+        $this->assertArrayHasKey('MIME-Version', $headers);
+        $this->assertCount(8, $headers);
+        $this->assertEquals('kolab at domain.tld', $headers['To']);
+
+        // test set_header()
+        $message->set_header('to', 'test at domain.tld');
+        $headers = $message->headers();
+
+        $this->assertCount(8, $headers);
+        $this->assertEquals('test at domain.tld', $headers['To']);
+    }
+
+    /**
+     * Test message parsing
+     */
+    function test_source()
+    {
+        $source  = file_get_contents(TESTS_DIR . '/src/mail.plain');
+        $message = new kolab_sync_message($source);
+        $result  = $message->source();
+
+        $this->assertEquals($source, str_replace("\r\n", "\n", $result));
+    }
+
+    /**
+     * Test adding attachments to the message
+     */
+    function test_attachment()
+    {
+        $source = file_get_contents(TESTS_DIR . '/src/mail.plain');
+        $mixed  = file_get_contents(TESTS_DIR . '/src/mail.plain.mixed');
+        $mixed2 = file_get_contents(TESTS_DIR . '/src/mail.mixed');
+
+        // test adding attachment to text/plain message
+        $message = new kolab_sync_message($source);
+        $message->add_attachment('aaa', array(
+            'content_type' => 'text/plain',
+            'encoding'     => '8bit',
+        ));
+
+        $result = $message->source();
+        $result = str_replace("\r\n", "\n", $result);
+        if (preg_match('/boundary="([^"]+)"/', $result, $m)) {
+            $mixed = str_replace('BOUNDARY', $m[1], $mixed);
+        }
+
+        $this->assertEquals($mixed, $result);
+
+        // test adding attachment to multipart/mixed message
+        $message = new kolab_sync_message($mixed);
+        $message->add_attachment('aaa', array(
+            'content_type' => 'text/plain',
+            'encoding'     => 'base64',
+        ));
+
+        $result = $message->source();
+        $result = str_replace("\r\n", "\n", $result);
+        if (preg_match('/boundary="([^"]+)"/', $result, $m)) {
+            $mixed2 = str_replace('BOUNDARY', $m[1], $mixed2);
+        }
+
+        $this->assertEquals($mixed2, $result);
+    }
+
+    /**
+     * Test appending a text to the message
+     */
+    function test_append()
+    {
+        // test appending text to text/plain message
+        $source = file_get_contents(TESTS_DIR . '/src/mail.plain');
+        $append = file_get_contents(TESTS_DIR . '/src/mail.plain.append');
+
+        $message = new kolab_sync_message($source);
+        $message->append('a');
+
+        $result  = $message->source();
+        $result  = str_replace("\r\n", "\n", $result);
+        $this->assertEquals($append, $result);
+    }
+}
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index 34fab9a..e49675a 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -5,6 +5,7 @@
         <testsuite name="All Tests">
             <file>body_converter.php</file>
             <file>data.php</file>
+            <file>message.php</file>
         </testsuite>
     </testsuites>
 </phpunit>
diff --git a/tests/src/mail.alternative b/tests/src/mail.alternative
new file mode 100644
index 0000000..555dda6
--- /dev/null
+++ b/tests/src/mail.alternative
@@ -0,0 +1,27 @@
+MIME-Version: 1.0
+content-class:
+From: 
+Subject: eee
+Date: Sun, 2 Sep 2012 11:41:14 +0200
+Importance: normal
+X-Priority: 3
+To: User <user at domain.tld>
+Content-Type: multipart/alternative;
+	boundary="_3EE5AD33-49F4-2808-9917-6969FE639CA7_"
+
+--_3EE5AD33-49F4-2808-9917-6969FE639CA7_
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset="iso-8859-1"
+
+tt=
+
+--_3EE5AD33-49F4-2808-9917-6969FE639CA7_
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/html; charset="iso-8859-1"
+
+<HTML><HEAD><META HTTP-EQUIV=3D'Content-Type' CONTENT=3D'text/html; charset=
+=3Diso-8859-1'></HEAD><BODY><SPAN style=3D'FONT-SIZE: 10pt; FONT-FAMILY: Ar=
+ial; FONT-WEIGHT:Normal;'>tt</SPAN></BODY></HTML>=
+
+--_3EE5AD33-49F4-2808-9917-6969FE639CA7_--
+
diff --git a/tests/src/mail.alternative2 b/tests/src/mail.alternative2
new file mode 100644
index 0000000..bacf4fc
--- /dev/null
+++ b/tests/src/mail.alternative2
@@ -0,0 +1,30 @@
+Date: Thu, 16 Aug 2012 07:38:43 +0000
+Subject: Re: test
+Message-ID: <1h4e6qv8o4ib0mhoj2hmyj0s.1345102723450 at email.android.com>
+From: user at domain.tld
+To: Kolab User <kolab at domain.tld>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="--_com.android.email_15071870071490"
+
+----_com.android.email_15071870071490
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+Q3NkZnNkZnNkZgoKIkEuTC5FLkMiIDxhbGVjQGFsZWMucGw+IHdyb3RlOgoKPnRlc3QgLS0gQWxl
+a3NhbmRlciAnQS5MLkUuQycgTWFjaG5pYWsgTEFOIE1hbmFnZW1lbnQgU3lzdGVtIERldmVsb3Bl
+ciBbaHR0cDovL2xtcy5vcmcucGxdIFJvdW5kY3ViZSBXZWJtYWlsIERldmVsb3BlciBbaHR0cDov
+L3JvdW5kY3ViZS5uZXRdIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t
+LS0tLS0tLS0tLSBQR1A6IDE5MzU5REMxIEBAIEdHOiAyMjc1MjUyIEBAIFdXVzogaHR0cDovL2Fs
+ZWMucGwg
+----_com.android.email_15071870071490
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: base64
+
+Q3NkZnNkZnNkZjxicj48YnI+JnF1b3Q7QS5MLkUuQyZxdW90OyAmbHQ7YWxlY0BhbGVjLnBsJmd0
+OyB3cm90ZTo8YnI+PGJyPjxwcmU+dGVzdAoKLS0gCkFsZWtzYW5kZXIgJ0EuTC5FLkMnIE1hY2hu
+aWFrCkxBTiBNYW5hZ2VtZW50IFN5c3RlbSBEZXZlbG9wZXIgW2h0dHA6Ly9sbXMub3JnLnBsXQpS
+b3VuZGN1YmUgV2VibWFpbCBEZXZlbG9wZXIgIFtodHRwOi8vcm91bmRjdWJlLm5ldF0KLS0tLS0t
+LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tClBHUDogMTkzNTlE
+QzEgQEAgR0c6IDIyNzUyNTIgQEAgV1dXOiBodHRwOi8vPHNwYW4gc3R5bGU9ImJhY2tncm91bmQt
+Y29sb3I6ICNmZmZmMDAiPmFsZWM8L3NwYW4+LnBsCjwvcHJlPg==
+----_com.android.email_15071870071490--
diff --git a/tests/src/mail.mixed b/tests/src/mail.mixed
new file mode 100644
index 0000000..be5f4fb
--- /dev/null
+++ b/tests/src/mail.mixed
@@ -0,0 +1,24 @@
+Date: Thu, 09 Aug 2012 13:18:31 +0000
+Subject: Fwd: test html xx
+Message-ID: <qma5x35ckynysjn2ee1jwwn8.1344518311869 at email.android.com>
+From: user at domain.tld
+To: kolab at domain.tld
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="BOUNDARY"
+
+--BOUNDARY
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+ZWVl
+--BOUNDARY
+Content-Transfer-Encoding: 8bit
+Content-Type: text/plain
+
+aaa
+--BOUNDARY
+Content-Transfer-Encoding: base64
+Content-Type: text/plain
+
+YWFh
+--BOUNDARY
diff --git a/tests/src/mail.plain b/tests/src/mail.plain
new file mode 100644
index 0000000..9b6129e
--- /dev/null
+++ b/tests/src/mail.plain
@@ -0,0 +1,10 @@
+Date: Thu, 09 Aug 2012 13:18:31 +0000
+Subject: Fwd: test html xx
+Message-ID: <qma5x35ckynysjn2ee1jwwn8.1344518311869 at email.android.com>
+From: user at domain.tld
+To: kolab at domain.tld
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+ZWVl
diff --git a/tests/src/mail.plain.append b/tests/src/mail.plain.append
new file mode 100644
index 0000000..ddffadf
--- /dev/null
+++ b/tests/src/mail.plain.append
@@ -0,0 +1,10 @@
+Date: Thu, 09 Aug 2012 13:18:31 +0000
+Subject: Fwd: test html xx
+Message-ID: <qma5x35ckynysjn2ee1jwwn8.1344518311869 at email.android.com>
+From: user at domain.tld
+To: kolab at domain.tld
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+ZWVlYQ==
diff --git a/tests/src/mail.plain.mixed b/tests/src/mail.plain.mixed
new file mode 100644
index 0000000..e7bc4e2
--- /dev/null
+++ b/tests/src/mail.plain.mixed
@@ -0,0 +1,19 @@
+Date: Thu, 09 Aug 2012 13:18:31 +0000
+Subject: Fwd: test html xx
+Message-ID: <qma5x35ckynysjn2ee1jwwn8.1344518311869 at email.android.com>
+From: user at domain.tld
+To: kolab at domain.tld
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="BOUNDARY"
+
+--BOUNDARY
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+ZWVl
+--BOUNDARY
+Content-Transfer-Encoding: 8bit
+Content-Type: text/plain
+
+aaa
+--BOUNDARY





More information about the commits mailing list