diff --git a/lib/Horde/ActiveSync/Device.php b/lib/Horde/ActiveSync/Device.php index 1ac16b4e..35a598ad 100644 --- a/lib/Horde/ActiveSync/Device.php +++ b/lib/Horde/ActiveSync/Device.php @@ -12,10 +12,21 @@ /** * Horde_ActiveSync_Device:: Wraps all functionality related to device data. * + * This class is the single home for client-specific behavior: + * + * - Hard client-conditional branches in request handlers, exporters or + * state code MUST go through hasQuirk() with a QUIRK_* constant defined + * here — never as inline deviceType/userAgent string comparisons at the + * call site. + * - Workarounds that are safe and correct for every client (e.g. output + * keepalive for clients with hard read timeouts) stay client-agnostic; + * the client that motivated them is named in a comment only. + * * @license http://www.horde.org/licenses/gpl GPLv2 * * @copyright 2010-2020 Horde LLC (http://www.horde.org) * @author Michael J Rubinsky + * @author Torben Dannhauer * @package ActiveSync * * @property string $id The device id. @@ -98,6 +109,14 @@ class Horde_ActiveSync_Device */ public const QUIRK_SUPPORTS_TNEF = 3; + /** + * Outlook 2013 does not duplicate the destination email during + * MOVEITEMS requests; it reassigns the existing email the new UID + * instead, so the ADD command for the moved message must not be sent + * to clients with this quirk. + */ + public const QUIRK_REASSIGNS_UID_ON_MOVE = 4; + /** * Device properties. * @@ -723,6 +742,9 @@ public function hasQuirk($quirk) || strpos($this->deviceType, 'WindowsOutlook') != false || strpos($this->userAgent, 'Outlook') !== false); + case self::QUIRK_REASSIGNS_UID_ON_MOVE: + return $this->deviceType == 'WindowsOutlook15'; + default: return false; } diff --git a/lib/Horde/ActiveSync/State/Base.php b/lib/Horde/ActiveSync/State/Base.php index 91760902..dda34e7b 100644 --- a/lib/Horde/ActiveSync/State/Base.php +++ b/lib/Horde/ActiveSync/State/Base.php @@ -768,13 +768,14 @@ public function getChanges(array $options = []) // this stuff. (Needs BC breaking changes in // storage/state classes). - // OL2013 is broken and duplicates the destination - // email during MOVEITEMS requests (instead it - // reassigns the existing email the new UID). Don't - // send the ADD command for these changes. + // Clients with QUIRK_REASSIGNS_UID_ON_MOVE (OL2013) + // do not duplicate the destination email during + // MOVEITEMS requests (they reassign the existing + // email the new UID). Don't send the ADD command + // for these changes. if ($changes[$i]['type'] == Horde_ActiveSync::CHANGE_TYPE_CHANGE && $changes[$i]['flags'] == Horde_ActiveSync::FLAG_NEWMESSAGE - && $this->_deviceInfo->deviceType != 'WindowsOutlook15') { + && !$this->_deviceInfo->hasQuirk(Horde_ActiveSync_Device::QUIRK_REASSIGNS_UID_ON_MOVE)) { $this->_changes[] = $changes[$i]; continue; } diff --git a/test/unit/Horde/ActiveSync/DeviceTest.php b/test/unit/Horde/ActiveSync/DeviceTest.php index 30120a27..f2318ade 100644 --- a/test/unit/Horde/ActiveSync/DeviceTest.php +++ b/test/unit/Horde/ActiveSync/DeviceTest.php @@ -294,6 +294,28 @@ public function testSupported() $this->assertEquals($device->supported, ['contacts' => ['one', 'two']]); } + public function testReassignsUidOnMoveQuirk() + { + $state = $this->getMockBuilder('Horde_ActiveSync_State_Base')->disableOriginalConstructor()->getMock(); + + // Outlook 2013 has the quirk. + $fixture = [ + 'deviceType' => 'WindowsOutlook15', + 'userAgent' => 'Outlook/15.0 (15.0.4675.1000; MSI; x86)', + ]; + $device = new Horde_ActiveSync_Device($state, $fixture); + $this->assertTrue($device->hasQuirk(Horde_ActiveSync_Device::QUIRK_REASSIGNS_UID_ON_MOVE)); + + // Other clients do not. + $fixture = [ + 'deviceType' => 'iPhone', + 'userAgent' => 'Apple-iPhone6C1/1104.201', + 'properties' => [Horde_ActiveSync_Device::OS => 'iOS 9.0.2 13A452'], + ]; + $device = new Horde_ActiveSync_Device($state, $fixture); + $this->assertFalse($device->hasQuirk(Horde_ActiveSync_Device::QUIRK_REASSIGNS_UID_ON_MOVE)); + } + public function testIssetReportsNestedDeviceProperties() { $state = $this->getMockBuilder('Horde_ActiveSync_State_Base')->disableOriginalConstructor()->getMock();