diff --git a/lib/ext/Syncroton/Command/SendMail.php b/lib/ext/Syncroton/Command/SendMail.php index 7fff25e..6307c81 100644 --- a/lib/ext/Syncroton/Command/SendMail.php +++ b/lib/ext/Syncroton/Command/SendMail.php @@ -1,93 +1,125 @@ + * @author Aleksander Machniak */ /** - * class to handle ActiveSync Sendmail command + * class to handle ActiveSync SendMail command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_SendMail extends Syncroton_Command_Wbxml { - protected $_defaultNameSpace = 'uri:ComposeMail'; - protected $_documentElement = 'SendMail'; + protected $_defaultNameSpace = 'uri:ComposeMail'; + protected $_documentElement = 'SendMail'; + protected $_mime; protected $_saveInSent; - protected $_source; - protected $_replaceMime = false; - + protected $_source; + protected $_replaceMime = false; + /** - * process the XML file and add, change, delete or fetches data - * - * @return resource + * Process the XML file and add, change, delete or fetches data */ public function handle() { if ($this->_requestParameters['contentType'] == 'message/rfc822') { $this->_mime = $this->_requestBody; $this->_saveInSent = $this->_requestParameters['saveInSent']; $this->_replaceMime = false; - - $this->_source = array( - 'collectionId' => $this->_requestParameters['collectionId'], - 'itemId' => $this->_requestParameters['itemId'], - 'instanceId' => null - ); - - } else { + + $this->_source = array( + 'collectionId' => $this->_requestParameters['collectionId'], + 'itemId' => $this->_requestParameters['itemId'], + 'instanceId' => null + ); + + } else if ($this->_requestBody) { $xml = simplexml_import_dom($this->_requestBody); - + $this->_mime = (string) $xml->Mime; $this->_saveInSent = isset($xml->SaveInSentItems); $this->_replaceMime = isset($xml->ReplaceMime); - + if (isset ($xml->Source)) { if ($xml->Source->LongId) { $this->_source = (string)$xml->Source->LongId; } else { $this->_source = array( 'collectionId' => (string)$xml->Source->FolderId, 'itemId' => (string)$xml->Source->ItemId, 'instanceId' => isset($xml->Source->InstanceId) ? (string)$xml->Source->InstanceId : null ); } } } - - if ($this->_logger instanceof Zend_Log) - $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " saveInSent: " . (int)$this->_saveInSent); + + if (empty($this->_mime)) { + if ($this->_logger instanceof Zend_Log) + $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " Sending email failed: Empty input"); + + + if (version_compare($this->_device->acsversion, '14.0', '<')) { + header("HTTP/1.1 400 Invalid content"); + die; + } + + $response_type = 'Syncroton_Model_' . $this->_documentElement; + $response = new $response_type(array( + 'status' => Syncroton_Exception_Status::INVALID_CONTENT, + )); + + $response->appendXML($this->_outputDom->documentElement, $this->_device); + + return $this->_outputDom; + } + + + if ($this->_logger instanceof Zend_Log) + $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " saveInSent: " . (int)$this->_saveInSent); } - + /** * this function generates the response for the client - * - * @return void + * + * @return void|DOMDocument */ public function getResponse() { $dataController = Syncroton_Data_Factory::factory(Syncroton_Data_Factory::CLASS_EMAIL, $this->_device, $this->_syncTimeStamp); try { - $dataController->sendEmail($this->_mime, $this->_saveInSent); + $this->sendMail($dataController); } catch (Syncroton_Exception_Status $ses) { if ($this->_logger instanceof Zend_Log) $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " Sending email failed: " . $ses->getMessage()); - $response = new Syncroton_Model_SendMail(array( + $response_type = 'Syncroton_Model_' . $this->_documentElement; + $response = new $response_type(array( 'status' => $ses->getCode(), )); $response->appendXML($this->_outputDom->documentElement, $this->_device); return $this->_outputDom; } } + + /** + * Execute email sending method of data controller + * To be overwritten by SmartForward and SmartReply command handlers + */ + protected function sendMail($dataController) + { + $dataController->sendEmail($this->_mime, $this->_saveInSent); + } } diff --git a/lib/ext/Syncroton/Command/SmartForward.php b/lib/ext/Syncroton/Command/SmartForward.php index 4a9d841..be41202 100644 --- a/lib/ext/Syncroton/Command/SmartForward.php +++ b/lib/ext/Syncroton/Command/SmartForward.php @@ -1,47 +1,30 @@ */ /** * class to handle ActiveSync SmartForward command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_SmartForward extends Syncroton_Command_SendMail { protected $_defaultNameSpace = 'uri:ComposeMail'; protected $_documentElement = 'SmartForward'; /** - * this function generates the response for the client - * - * @return void + * Execute email sending method of data controller */ - public function getResponse() + protected function sendMail($dataController) { - $dataController = Syncroton_Data_Factory::factory(Syncroton_Data_Factory::CLASS_EMAIL, $this->_device, $this->_syncTimeStamp); - - try { - $dataController->forwardEmail($this->_source, $this->_mime, $this->_saveInSent, $this->_replaceMime); - } catch (Syncroton_Exception_Status $ses) { - if ($this->_logger instanceof Zend_Log) - $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " Sending email failed: " . $ses->getMessage()); - - $response = new Syncroton_Model_SmartForward(array( - 'status' => $ses->getCode(), - )); - - $response->appendXML($this->_outputDom->documentElement, $this->_device); - - return $this->_outputDom; - } + $dataController->forwardEmail($this->_source, $this->_mime, $this->_saveInSent, $this->_replaceMime); } } diff --git a/lib/ext/Syncroton/Command/SmartReply.php b/lib/ext/Syncroton/Command/SmartReply.php index 59149ee..43ec9b7 100644 --- a/lib/ext/Syncroton/Command/SmartReply.php +++ b/lib/ext/Syncroton/Command/SmartReply.php @@ -1,47 +1,30 @@ */ /** * class to handle ActiveSync SmartReply command * * @package Syncroton * @subpackage Command */ class Syncroton_Command_SmartReply extends Syncroton_Command_SendMail { protected $_defaultNameSpace = 'uri:ComposeMail'; protected $_documentElement = 'SmartReply'; /** - * this function generates the response for the client - * - * @return void + * Execute email sending method of data controller */ - public function getResponse() + protected function sendMail($dataController) { - $dataController = Syncroton_Data_Factory::factory(Syncroton_Data_Factory::CLASS_EMAIL, $this->_device, $this->_syncTimeStamp); - - try { - $dataController->replyEmail($this->_source, $this->_mime, $this->_saveInSent, $this->_replaceMime); - } catch (Syncroton_Exception_Status $ses) { - if ($this->_logger instanceof Zend_Log) - $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " Sending email failed: " . $ses->getMessage()); - - $response = new Syncroton_Model_SmartReply(array( - 'status' => $ses->getCode(), - )); - - $response->appendXML($this->_outputDom->documentElement, $this->_device); - - return $this->_outputDom; - } + $dataController->replyEmail($this->_source, $this->_mime, $this->_saveInSent, $this->_replaceMime); } } diff --git a/lib/ext/Syncroton/Server.php b/lib/ext/Syncroton/Server.php index d60ab9a..f65abba 100644 --- a/lib/ext/Syncroton/Server.php +++ b/lib/ext/Syncroton/Server.php @@ -1,452 +1,453 @@ */ /** * class to handle incoming http ActiveSync requests * * @package Syncroton */ class Syncroton_Server { const PARAMETER_ATTACHMENTNAME = 0; const PARAMETER_COLLECTIONID = 1; const PARAMETER_ITEMID = 3; const PARAMETER_OPTIONS = 7; const MAX_HEARTBEAT_INTERVAL = 3540; // 59 minutes protected $_body; /** * informations about the currently device * * @var Syncroton_Backend_IDevice */ protected $_deviceBackend; /** * @var Zend_Log */ protected $_logger; /** * @var Zend_Controller_Request_Http */ protected $_request; protected $_userId; public function __construct($userId, Zend_Controller_Request_Http $request = null, $body = null) { if (Syncroton_Registry::isRegistered('loggerBackend')) { $this->_logger = Syncroton_Registry::get('loggerBackend'); } $this->_userId = $userId; $this->_request = $request instanceof Zend_Controller_Request_Http ? $request : new Zend_Controller_Request_Http(); $this->_body = $body !== null ? $body : fopen('php://input', 'r'); $this->_deviceBackend = Syncroton_Registry::getDeviceBackend(); } public function handle() { if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST METHOD: ' . $this->_request->getMethod()); switch($this->_request->getMethod()) { case 'OPTIONS': $this->_handleOptions(); break; case 'POST': $this->_handlePost(); break; case 'GET': echo "It works!
Your userid is: {$this->_userId} and your IP address is: {$_SERVER['REMOTE_ADDR']}."; break; } } /** * handle options request */ protected function _handleOptions() { $command = new Syncroton_Command_Options(); $this->_sendHeaders($command->getHeaders()); } protected function _sendHeaders(array $headers) { foreach ($headers as $name => $value) { header($name . ': ' . $value); } } /** * handle post request */ protected function _handlePost() { $requestParameters = $this->_getRequestParameters($this->_request); if ($this->_logger instanceof Zend_Log) $this->_logger->debug(__METHOD__ . '::' . __LINE__ . ' REQUEST ' . print_r($requestParameters, true)); $className = 'Syncroton_Command_' . $requestParameters['command']; if(!class_exists($className)) { if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " command not supported: " . $requestParameters['command']); header("HTTP/1.1 501 not implemented"); return; } // get user device $device = $this->_getUserDevice($this->_userId, $requestParameters); if ($requestParameters['contentType'] == 'application/vnd.ms-sync.wbxml' || $requestParameters['contentType'] == 'application/vnd.ms-sync') { // decode wbxml request try { $decoder = new Syncroton_Wbxml_Decoder($this->_body); $requestBody = $decoder->decode(); if ($this->_logger instanceof Zend_Log) { $requestBody->formatOutput = true; $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " xml request:\n" . $requestBody->saveXML()); } } catch(Syncroton_Wbxml_Exception_UnexpectedEndOfFile $e) { $requestBody = NULL; } } else { $requestBody = $this->_body; } header("MS-Server-ActiveSync: 14.00.0536.000"); // avoid sending HTTP header "Content-Type: text/html" for empty sync responses ini_set('default_mimetype', 'application/vnd.ms-sync.wbxml'); try { $command = new $className($requestBody, $device, $requestParameters); - $command->handle(); - - $response = $command->getResponse(); - + $response = $command->handle(); + + if (!$response) { + $response = $command->getResponse(); + } } catch (Syncroton_Exception_ProvisioningNeeded $sepn) { if ($this->_logger instanceof Zend_Log) $this->_logger->info(__METHOD__ . '::' . __LINE__ . " provisioning needed"); header("HTTP/1.1 449 Retry after sending a PROVISION command"); if (version_compare($device->acsversion, '14.0', '>=')) { $response = $sepn->domDocument; } else { // pre 14.0 method return; } } catch (Exception $e) { if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " unexpected exception occured: " . get_class($e)); if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " exception message: " . $e->getMessage()); if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " " . $e->getTraceAsString()); header("HTTP/1.1 500 Internal server error"); return; } if ($response instanceof DOMDocument) { if ($this->_logger instanceof Zend_Log) { $this->_logDomDocument(Zend_Log::DEBUG, $response, __METHOD__, __LINE__); } if (isset($command) && $command instanceof Syncroton_Command_ICommand) { $this->_sendHeaders($command->getHeaders()); } $outputStream = fopen("php://temp", 'r+'); $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3); try { $encoder->encode($response); } catch (Syncroton_Wbxml_Exception $swe) { if ($this->_logger instanceof Zend_Log) { $this->_logger->err(__METHOD__ . '::' . __LINE__ . " Could not encode output: " . $swe); $this->_logDomDocument(Zend_Log::WARN, $response, __METHOD__, __LINE__); } header("HTTP/1.1 500 Internal server error"); return; } if ($requestParameters['acceptMultipart'] == true) { $parts = $command->getParts(); // output multipartheader $bodyPartCount = 1 + count($parts); // number of parts (4 bytes) $header = pack('i', $bodyPartCount); $partOffset = 4 + (($bodyPartCount * 2) * 4); // wbxml body start and length $streamStat = fstat($outputStream); $header .= pack('ii', $partOffset, $streamStat['size']); $partOffset += $streamStat['size']; // calculate start and length of parts foreach ($parts as $partId => $partStream) { rewind($partStream); $streamStat = fstat($partStream); // part start and length $header .= pack('ii', $partOffset, $streamStat['size']); $partOffset += $streamStat['size']; } echo $header; } // output body rewind($outputStream); fpassthru($outputStream); // output multiparts if (isset($parts)) { foreach ($parts as $partStream) { rewind($partStream); fpassthru($partStream); } } } } /** * write (possible big) DOMDocument in smaller chunks to log file * * @param unknown $priority * @param DOMDocument $dom * @param string $method * @param int $line */ protected function _logDomDocument($priority, DOMDocument $dom, $method, $line) { $loops = 0; $tempStream = fopen('php://temp/maxmemory:5242880', 'r+'); $dom->formatOutput = true; fwrite($tempStream, $dom->saveXML()); $dom->formatOutput = false; rewind($tempStream); // log data in 1MByte chunks while (!feof($tempStream)) { $this->_logger->log($method . '::' . $line . " xml response($loops):\n" . fread($tempStream, 1048576), $priority); $loops++; } fclose($tempStream); } /** * return request params * * @return array */ protected function _getRequestParameters(Zend_Controller_Request_Http $request) { if (strpos($request->getRequestUri(), '&') === false) { $commands = array( 0 => 'Sync', 1 => 'SendMail', 2 => 'SmartForward', 3 => 'SmartReply', 4 => 'GetAttachment', 9 => 'FolderSync', 10 => 'FolderCreate', 11 => 'FolderDelete', 12 => 'FolderUpdate', 13 => 'MoveItems', 14 => 'GetItemEstimate', 15 => 'MeetingResponse', 16 => 'Search', 17 => 'Settings', 18 => 'Ping', 19 => 'ItemOperations', 20 => 'Provision', 21 => 'ResolveRecipients', 22 => 'ValidateCert' ); $requestParameters = substr($request->getRequestUri(), strpos($request->getRequestUri(), '?')); $stream = fopen("php://temp", 'r+'); fwrite($stream, base64_decode($requestParameters)); rewind($stream); // unpack the first 4 bytes $unpacked = unpack('CprotocolVersion/Ccommand/vlocale', fread($stream, 4)); // 140 => 14.0 $protocolVersion = substr($unpacked['protocolVersion'], 0, -1) . '.' . substr($unpacked['protocolVersion'], -1); $command = $commands[$unpacked['command']]; $locale = $unpacked['locale']; // unpack deviceId $length = ord(fread($stream, 1)); if ($length > 0) { $toUnpack = fread($stream, $length); $unpacked = unpack("H" . ($length * 2) . "string", $toUnpack); $deviceId = $unpacked['string']; } // unpack policyKey $length = ord(fread($stream, 1)); if ($length > 0) { $unpacked = unpack('Vstring', fread($stream, $length)); $policyKey = $unpacked['string']; } // unpack device type $length = ord(fread($stream, 1)); if ($length > 0) { $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $deviceType = $unpacked['string']; } while (! feof($stream)) { $tag = ord(fread($stream, 1)); $length = ord(fread($stream, 1)); switch ($tag) { case self::PARAMETER_ATTACHMENTNAME: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $attachmentName = $unpacked['string']; break; case self::PARAMETER_COLLECTIONID: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $collectionId = $unpacked['string']; break; case self::PARAMETER_ITEMID: $unpacked = unpack('A' . $length . 'string', fread($stream, $length)); $itemId = $unpacked['string']; break; case self::PARAMETER_OPTIONS: $options = ord(fread($stream, 1)); $saveInSent = !!($options & 0x01); $acceptMultiPart = !!($options & 0x02); break; default: if ($this->_logger instanceof Zend_Log) $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " found unhandled command parameters"); } } $result = array( 'protocolVersion' => $protocolVersion, 'command' => $command, 'deviceId' => $deviceId, 'deviceType' => isset($deviceType) ? $deviceType : null, 'policyKey' => isset($policyKey) ? $policyKey : null, 'saveInSent' => isset($saveInSent) ? $saveInSent : false, 'collectionId' => isset($collectionId) ? $collectionId : null, 'itemId' => isset($itemId) ? $itemId : null, 'attachmentName' => isset($attachmentName) ? $attachmentName : null, 'acceptMultipart' => isset($acceptMultiPart) ? $acceptMultiPart : false ); } else { $result = array( 'protocolVersion' => $request->getServer('HTTP_MS_ASPROTOCOLVERSION'), 'command' => $request->getQuery('Cmd'), 'deviceId' => $request->getQuery('DeviceId'), 'deviceType' => $request->getQuery('DeviceType'), 'policyKey' => $request->getServer('HTTP_X_MS_POLICYKEY'), 'saveInSent' => $request->getQuery('SaveInSent') == 'T', 'collectionId' => $request->getQuery('CollectionId'), 'itemId' => $request->getQuery('ItemId'), 'attachmentName' => $request->getQuery('AttachmentName'), 'acceptMultipart' => $request->getServer('HTTP_MS_ASACCEPTMULTIPART') == 'T' ); } $result['userAgent'] = $request->getServer('HTTP_USER_AGENT', $result['deviceType']); $result['contentType'] = $request->getServer('CONTENT_TYPE'); return $result; } /** * get existing device of owner or create new device for owner * * @param unknown_type $ownerId * @param unknown_type $deviceId * @param unknown_type $deviceType * @param unknown_type $userAgent * @param unknown_type $protocolVersion * @return Syncroton_Model_Device */ protected function _getUserDevice($ownerId, $requestParameters) { try { $device = $this->_deviceBackend->getUserDevice($ownerId, $requestParameters['deviceId']); $device->useragent = $requestParameters['userAgent']; $device->acsversion = $requestParameters['protocolVersion']; if ($device->isDirty()) { $device = $this->_deviceBackend->update($device); } } catch (Syncroton_Exception_NotFound $senf) { $device = $this->_deviceBackend->create(new Syncroton_Model_Device(array( 'owner_id' => $ownerId, 'deviceid' => $requestParameters['deviceId'], 'devicetype' => $requestParameters['deviceType'], 'useragent' => $requestParameters['userAgent'], 'acsversion' => $requestParameters['protocolVersion'], 'policyId' => Syncroton_Registry::isRegistered(Syncroton_Registry::DEFAULT_POLICY) ? Syncroton_Registry::get(Syncroton_Registry::DEFAULT_POLICY) : null ))); } return $device; } public static function validateSession() { $validatorFunction = Syncroton_Registry::getSessionValidator(); return $validatorFunction(); } }