Page MenuHomePhorge

D5096.1775268261.diff
No OneTemporary

Authored By
Unknown
Size
19 KB
Referenced Files
None
Subscribers
None

D5096.1775268261.diff

diff --git a/config/config.inc.php.dist b/config/config.inc.php.dist
--- a/config/config.inc.php.dist
+++ b/config/config.inc.php.dist
@@ -10,11 +10,9 @@
$config['activesync_log_file'] = null;
// Type of ActiveSync cache. Supported values: 'db', 'apc' and 'memcache'.
-// Note: This is only for some additional data like timezones mapping.
$config['activesync_cache'] = 'db';
-// lifetime of ActiveSync cache
-// possible units: s, m, h, d, w
+// Lifetime of the ActiveSync cache. Supported units: s, m, h, d, w
$config['activesync_cache_ttl'] = '1d';
// Type of ActiveSync Auth cache. Supported values: 'db', 'apc' and 'memcache'.
diff --git a/lib/ext/Syncroton/Command/SendMail.php b/lib/ext/Syncroton/Command/SendMail.php
--- a/lib/ext/Syncroton/Command/SendMail.php
+++ b/lib/ext/Syncroton/Command/SendMail.php
@@ -23,6 +23,7 @@
protected $_defaultNameSpace = 'uri:ComposeMail';
protected $_documentElement = 'SendMail';
+ protected $_clientId;
protected $_mime;
protected $_saveInSent;
protected $_source;
@@ -48,6 +49,7 @@
} elseif ($this->_requestBody) {
$xml = simplexml_import_dom($this->_requestBody);
+ $this->_clientId = (string) $xml->ClientId;
$this->_mime = (string) $xml->Mime;
$this->_saveInSent = isset($xml->SaveInSentItems);
$this->_replaceMime = isset($xml->ReplaceMime);
@@ -125,6 +127,6 @@
*/
protected function sendMail($dataController)
{
- $dataController->sendEmail($this->_mime, $this->_saveInSent);
+ $dataController->sendEmail($this->_mime, $this->_saveInSent, $this->_clientId);
}
}
diff --git a/lib/ext/Syncroton/Command/SmartForward.php b/lib/ext/Syncroton/Command/SmartForward.php
--- a/lib/ext/Syncroton/Command/SmartForward.php
+++ b/lib/ext/Syncroton/Command/SmartForward.php
@@ -26,6 +26,12 @@
*/
protected function sendMail($dataController)
{
- $dataController->forwardEmail($this->_source, $this->_mime, $this->_saveInSent, $this->_replaceMime);
+ $dataController->forwardEmail(
+ $this->_source,
+ $this->_mime,
+ $this->_saveInSent,
+ $this->_replaceMime,
+ $this->_clientId
+ );
}
}
diff --git a/lib/ext/Syncroton/Command/SmartReply.php b/lib/ext/Syncroton/Command/SmartReply.php
--- a/lib/ext/Syncroton/Command/SmartReply.php
+++ b/lib/ext/Syncroton/Command/SmartReply.php
@@ -26,6 +26,6 @@
*/
protected function sendMail($dataController)
{
- $dataController->replyEmail($this->_source, $this->_mime, $this->_saveInSent, $this->_replaceMime);
+ $dataController->replyEmail($this->_source, $this->_mime, $this->_saveInSent, $this->_replaceMime, $this->_clientId);
}
}
diff --git a/lib/ext/Syncroton/Data/Email.php b/lib/ext/Syncroton/Data/Email.php
--- a/lib/ext/Syncroton/Data/Email.php
+++ b/lib/ext/Syncroton/Data/Email.php
@@ -31,7 +31,7 @@
* (non-PHPdoc)
* @see Syncroton_Data_IDataEmail::forwardEmail()
*/
- public function forwardEmail($source, $inputStream, $saveInSent, $replaceMime)
+ public function forwardEmail($source, $inputStream, $saveInSent, $replaceMime, $clientId)
{
if ($inputStream == 'triggerException') {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAILBOX_SERVER_OFFLINE);
@@ -59,7 +59,7 @@
* (non-PHPdoc)
* @see Syncroton_Data_IDataEmail::replyEmail()
*/
- public function replyEmail($source, $inputStream, $saveInSent, $replaceMime)
+ public function replyEmail($source, $inputStream, $saveInSent, $replaceMime, $clientId)
{
// forward email
}
@@ -78,7 +78,7 @@
* (non-PHPdoc)
* @see Syncroton_Data_IDataEmail::sendEmail()
*/
- public function sendEmail($inputStream, $saveInSent)
+ public function sendEmail($inputStream, $saveInSent, $clientId)
{
if ($inputStream == 'triggerException') {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAILBOX_SERVER_OFFLINE);
diff --git a/lib/ext/Syncroton/Data/IDataEmail.php b/lib/ext/Syncroton/Data/IDataEmail.php
--- a/lib/ext/Syncroton/Data/IDataEmail.php
+++ b/lib/ext/Syncroton/Data/IDataEmail.php
@@ -21,26 +21,31 @@
/**
* send an email
*
- * @param resource $inputStream
- * @param boolean $saveInSent
+ * @param resource $inputStream
+ * @param bool $saveInSent
+ * @param ?string $clientId
*/
- public function sendEmail($inputStream, $saveInSent);
+ public function sendEmail($inputStream, $saveInSent, $clientId);
/**
* forward an email
*
- * @param string|array $source is either a string(LongId) or an array with following properties collectionId, itemId and instanceId
- * @param string $inputStream
- * @param string $saveInSent
+ * @param string|array $source is either a string(LongId) or an array with following properties collectionId, itemId and instanceId
+ * @param string $inputStream
+ * @param string $saveInSent
+ * @param bool $replaceMime
+ * @param ?string $clientId
*/
- public function forwardEmail($source, $inputStream, $saveInSent, $replaceMime);
+ public function forwardEmail($source, $inputStream, $saveInSent, $replaceMime, $clientId);
/**
* reply to an email
*
- * @param string|array $source is either a string(LongId) or an array with following properties collectionId, itemId and instanceId
- * @param string $inputStream
- * @param string $saveInSent
+ * @param string|array $source is either a string(LongId) or an array with following properties collectionId, itemId and instanceId
+ * @param string $inputStream
+ * @param string $saveInSent
+ * @param bool $replaceMime
+ * @param ?string $clientId
*/
- public function replyEmail($source, $inputStream, $saveInSent, $replaceMime);
+ public function replyEmail($source, $inputStream, $saveInSent, $replaceMime, $clientId);
}
diff --git a/lib/kolab_sync_data_email.php b/lib/kolab_sync_data_email.php
--- a/lib/kolab_sync_data_email.php
+++ b/lib/kolab_sync_data_email.php
@@ -30,6 +30,9 @@
{
public const MAX_SEARCH_RESULT = 200;
+ protected const MAIL_SUBMITTED = 1;
+ protected const MAIL_DONE = 2;
+
/**
* Mapping from ActiveSync Email namespace fields
*/
@@ -823,30 +826,44 @@
* Send an email
*
* @param mixed $message MIME message
- * @param boolean $saveInSent Enables saving the sent message in Sent folder
+ * @param bool $saveInSent Enables saving the sent message in Sent folder
+ * @param ?string $clientId Message client-id
*
* @throws Syncroton_Exception_Status
*/
- public function sendEmail($message, $saveInSent)
+ public function sendEmail($message, $saveInSent, $clientId)
{
+ if (($status = $this->sentMailStatus($clientId, $cache, $cache_key)) === self::MAIL_DONE) {
+ throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MESSAGE_PREVIOUSLY_SENT);
+ }
+
if (!($message instanceof kolab_sync_message)) {
$message = new kolab_sync_message($message);
}
- $sent = $message->send($smtp_error);
+ // Snet the message (if not sent previously)
+ if (!$status) {
+ $sent = $message->send($smtp_error);
- if (!$sent) {
- throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAIL_SUBMISSION_FAILED);
+ if (!$sent) {
+ throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAIL_SUBMISSION_FAILED);
+ }
+
+ if (!empty($cache)) {
+ $cache->set($cache_key, self::MAIL_SUBMITTED);
+ }
}
// Save sent message in Sent folder
if ($saveInSent) {
- $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, $message->source(), '', false, ['SEEN']);
+ if (!$message->saveInSent()) {
+ throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
}
+
+ if (!empty($cache)) {
+ $cache->set($cache_key, self::MAIL_DONE);
+ }
}
/**
@@ -855,13 +872,18 @@
* @param array|string $itemId A string LongId or an array with following properties:
* collectionId, itemId and instanceId
* @param resource|string $body MIME message
- * @param boolean $saveInSent Enables saving the sent message in Sent folder
- * @param boolean $replaceMime If enabled, original message would be appended
+ * @param bool $saveInSent Enables saving the sent message in Sent folder
+ * @param bool $replaceMime If enabled, original message would be appended
+ * @param ?string $clientId Message client-id
*
* @throws Syncroton_Exception_Status
*/
- public function forwardEmail($itemId, $body, $saveInSent, $replaceMime)
+ public function forwardEmail($itemId, $body, $saveInSent, $replaceMime, $clientId)
{
+ if ($this->sentMailStatus($clientId) === self::MAIL_DONE) {
+ throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MESSAGE_PREVIOUSLY_SENT);
+ }
+
/*
@TODO:
The SmartForward command can be applied to a meeting. When SmartForward is applied to a recurring meeting,
@@ -903,7 +925,7 @@
}
// Send message
- $this->sendEmail($sync_msg, $saveInSent);
+ $this->sendEmail($sync_msg, $saveInSent, $clientId);
// Set FORWARDED flag on the replied message
if (empty($message->headers->flags['FORWARDED'])) {
@@ -918,13 +940,18 @@
* @param array|string $itemId A string LongId or an array with following properties:
* collectionId, itemId and instanceId
* @param resource|string $body MIME message
- * @param boolean $saveInSent Enables saving the sent message in Sent folder
- * @param boolean $replaceMime If enabled, original message would be appended
+ * @param bool $saveInSent Enables saving the sent message in Sent folder
+ * @param bool $replaceMime If enabled, original message would be appended
+ * @param ?string $clientId Message client-id
*
* @throws Syncroton_Exception_Status
*/
- public function replyEmail($itemId, $body, $saveInSent, $replaceMime)
+ public function replyEmail($itemId, $body, $saveInSent, $replaceMime, $clientId)
{
+ if ($this->sentMailStatus($clientId) === self::MAIL_DONE) {
+ throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MESSAGE_PREVIOUSLY_SENT);
+ }
+
$msg = $this->parseMessageId($itemId);
$message = $this->getObject($itemId);
@@ -954,7 +981,7 @@
}
// Send message
- $this->sendEmail($sync_msg, $saveInSent);
+ $this->sendEmail($sync_msg, $saveInSent, $clientId);
// Set ANSWERED flag on the replied message
if (empty($message->headers->flags['ANSWERED'])) {
@@ -1578,4 +1605,27 @@
return kolab_sync_message::fake_message($headers, $msg);
}
}
+
+ /**
+ * Check in the cache if specified message (client-id) has been previously processed
+ * and with what result. It's used to prevent a duplicate submission.
+ */
+ protected function sentMailStatus($clientId, &$cache = null, &$cache_key = null)
+ {
+ // Note: ClientId is set with ActiveSync version >= 14.0
+ if ($clientId === null || $clientId === '') {
+ return 0;
+ }
+
+ $engine = kolab_sync::get_instance();
+ $status = null;
+ $cache_key = "ClientId:{$clientId}";
+
+ if ($cache_type = $engine->config->get('activesync_cache', 'db')) {
+ $cache = $engine->get_cache('activesync_cache', $cache_type, '1d', false);
+ $status = $cache->get($cache_key);
+ }
+
+ return (int) $status;
+ }
}
diff --git a/lib/kolab_sync_message.php b/lib/kolab_sync_message.php
--- a/lib/kolab_sync_message.php
+++ b/lib/kolab_sync_message.php
@@ -216,13 +216,21 @@
unset($smtp_headers['Bcc']);
// send message
- if (!is_object($rcube->smtp)) {
- $rcube->smtp_init(true);
- }
+ if (isset($headers['X-Syncroton-Test'])
+ && preg_match('/smtp=(true|false)/i', $headers['X-Syncroton-Test'], $m)
+ ) {
+ $sent = $m[1] == 'true';
+ $smtp_response = [];
+ $smtp_error = 999;
+ } else {
+ 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();
+ $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) {
@@ -260,6 +268,38 @@
return $sent;
}
+ /**
+ * Save message in Sent folder
+ *
+ * @return bool True on success (or when the folder does not exist), False otherwise
+ */
+ public function saveInSent()
+ {
+ $engine = kolab_sync::get_instance();
+ $storage = $engine->get_storage();
+ $sent_folder = $engine->config->get('sent_mbox');
+
+ if (isset($this->headers['X-Syncroton-Test'])
+ && preg_match('/imap=(true|false)/i', $this->headers['X-Syncroton-Test'], $m)
+ ) {
+ return $m[1] == 'true';
+ }
+
+ if (strlen($sent_folder) && $storage->folder_exists($sent_folder)) {
+ $source = $this->source();
+ $uid = $storage->save_message($sent_folder, $source, '', false, ['SEEN']);
+
+ if (empty($uid)) {
+ rcube::raise_error(['code' => 500, 'type' => 'imap',
+ 'line' => __LINE__, 'file' => __FILE__,
+ 'message' => "Failed to save message in {$sent_folder}"], true, false);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
/**
* Parses the message source and fixes 8bit data for ActiveSync.
* This way any not UTF8 characters will be encoded before
diff --git a/tests/Sync/SendMailTest.php b/tests/Sync/SendMailTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Sync/SendMailTest.php
@@ -0,0 +1,112 @@
+<?php
+
+class SendMailTest extends Tests\SyncTestCase
+{
+ /**
+ * Test SendMail command
+ */
+ public function testSendMail()
+ {
+ $this->emptyTestFolder('Sent', 'mail');
+
+ $clientId = microtime();
+
+ $request = <<<EOF
+ <?xml version="1.0" encoding="utf-8"?>
+ <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/">
+ <SendMail xmlns="uri:ComposeMail">
+ <ClientId>{$clientId}</ClientId>
+ <SaveInSentItems />
+ <Mime>From: testuser1@kolab.org
+ To: testuser2@kolab.org
+ Subject: Test
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="iso-8859-1"
+ Message-ID: &lt;msg1@kolab.org&gt;
+ X-Syncroton-Test: smtp=true
+
+ This is the email body content.</Mime>
+ </SendMail>
+ EOF;
+
+ $response = $this->request($request, 'SendMail');
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertSame('', (string) $response->getBody());
+ $emails = $this->listEmails('Sent', '*');
+ $this->assertCount(1, $emails);
+ // TODO: Assert mail content
+ }
+
+ /**
+ * Test SendMail command
+ */
+ public function testSendMailErrorHandling()
+ {
+ $this->emptyTestFolder('Sent', 'mail');
+
+ $clientId = microtime();
+
+ $request = <<<EOF
+ <?xml version="1.0" encoding="utf-8"?>
+ <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/">
+ <SendMail xmlns="uri:ComposeMail">
+ <ClientId>{$clientId}</ClientId>
+ <SaveInSentItems />
+ <Mime>From: testuser1@kolab.org
+ To: testuser2@kolab.org
+ Subject: Test
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="iso-8859-1"
+ Message-ID: &lt;msg1@kolab.org&gt;
+ X-Syncroton-Test: smtp=false imap=true
+
+ This is the email body content.</Mime>
+ </SendMail>
+ EOF;
+
+ // Expect a SMTP error
+ $response = $this->request($request, 'SendMail');
+
+ $dom = $this->fromWbxml($response->getBody());
+ $xpath = $this->xpath($dom);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertSame('120', $xpath->query("//ns:SendMail/ns:Status")->item(0)->nodeValue);
+ $this->assertCount(0, $this->listEmails('Sent', '*'));
+
+ // Test IMAP error handling
+ $request = str_replace('smtp=false', 'smtp=true', $request);
+ $request = str_replace('imap=true', 'imap=false', $request);
+
+ $response = $this->request($request, 'SendMail');
+
+ $dom = $this->fromWbxml($response->getBody());
+ $xpath = $this->xpath($dom);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertSame('110', $xpath->query("//ns:SendMail/ns:Status")->item(0)->nodeValue);
+ $this->assertCount(0, $this->listEmails('Sent', '*'));
+
+ // Test no error
+ // smtp=false would cause error, but the submission should get skipped now
+ $request = str_replace('smtp=true', 'smtp=false', $request);
+ $request = str_replace('imap=false', '', $request);
+
+ $response = $this->request($request, 'SendMail');
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertSame('', (string) $response->getBody());
+ $this->assertCount(1, $this->listEmails('Sent', '*'));
+
+ // Send the same mail again, expect an error
+ $response = $this->request($request, 'SendMail');
+
+ $dom = $this->fromWbxml($response->getBody());
+ $xpath = $this->xpath($dom);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertSame('118', $xpath->query("//ns:SendMail/ns:Status")->item(0)->nodeValue);
+ $this->assertCount(1, $this->listEmails('Sent', '*'));
+ }
+}
diff --git a/tests/SyncTestCase.php b/tests/SyncTestCase.php
--- a/tests/SyncTestCase.php
+++ b/tests/SyncTestCase.php
@@ -418,6 +418,7 @@
$xpath->registerNamespace("AirSyncBase", "uri:AirSyncBase");
$xpath->registerNamespace("Calendar", "uri:Calendar");
$xpath->registerNamespace("Contacts", "uri:Contacts");
+ $xpath->registerNamespace("ComposeMail", "uri:ComposeMail");
$xpath->registerNamespace("Email", "uri:Email");
$xpath->registerNamespace("Email2", "uri:Email2");
$xpath->registerNamespace("Settings", "uri:Settings");

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 2:04 AM (5 h, 18 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18776292
Default Alt Text
D5096.1775268261.diff (19 KB)

Event Timeline