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