SOAP Exchange · 8 min read · Dec 31, 2025

Hablando de SOAP con Exchange

Hablando de SOAP con Exchange

Anteriormente, hablar con Exchange sin usar productos de Microsoft era prácticamente impensable. El protocolo binario MAPI es propietario y está mal documentado. Exchange soporta IMAP y POP, pero estos protocolos solo dan acceso a correos electrónicos, no al calendario, libreta de direcciones, listas de tareas, etc. Pero a partir de la versión 2007, Exchange ahora incluye una interfaz SOAP llamada Exchange Web Services, o EWS. Esta interfaz nos da acceso a las funciones necesarias para escribir clientes en cualquier lenguaje de programación en cualquier plataforma.

Este artículo describe un programa en PHP para buscar, eliminar e insertar elementos en un calendario de Exchange.

Resumen

SOAP es un estándar basado en XML para servicios web. PHP soporta SOAP en un módulo separado. Una parte de la especificación SOAP es WSDL, un lenguaje de definición de servicios web basado en XML que define los tipos de datos y las funciones disponibles. Las funciones y tipos de datos en EWS están en realidad muy bien documentados en MSDN: http://msdn.microsoft.com/en-us/library/bb204119.aspx. EWS utiliza el protocolo HTTPS para la comunicación, pero en lugar de autenticación básica, utiliza autenticación NTLM específica de Microsoft. PHP no soporta este protocolo con SOAP, pero, como veremos, podemos encontrar una solución.

El script

Una comunicación SOAP normal en PHP se ve algo así:

`$wsdl = “http://example.com/webservice/definition.wsdl”; $client = new SoapClient($wsdl); $request = 123; $response = $client->MyFunction($request);

En un servidor Exchange 2007, el archivo WSDL generalmente se encuentra en https://exchange.example.com/EWS/Services.wsdl. Para acceder a este archivo, necesitamos un nombre de usuario y una contraseña para un usuario válido en el servidor Exchange. Sin embargo, dado que Exchange utiliza autenticación NTLM, necesitamos hacer un envoltorio para SoapClient. La biblioteca CURL (también disponible como una biblioteca PHP) soporta autenticación NTLM, así que usaremos esto para hacer el envoltorio:

class NTLMSoapClient extends SoapClient { function __doRequest($request, $location, $action, $version) { $headers = array( 'Method: POST', 'Connection: Keep-Alive', 'User-Agent: PHP-SOAP-CURL', 'Content-Type: text/xml; charset=utf-8', 'SOAPAction: "'.$action.'"', ); $this->__last_request_headers = $headers; $ch = curl_init($location); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_POST, true ); curl_setopt($ch, CURLOPT_POSTFIELDS, $request); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_NTLM); curl_setopt($ch, CURLOPT_USERPWD, $this->user.':'.$this->password); $response = curl_exec($ch); return $response; } function __getLastRequestHeaders() { return implode("n", $this->__last_request_headers)."n"; } }

Esta clase anula la función doRequest de SoapClient para usar CURL para obtener el archivo WSDL. Dependiendo de tu instalación de PHP, es posible que necesites instalar el módulo PHP CURL para que esto funcione. Edit: Si experimentas errores de SoapClient, es posible que necesites desactivar la validación del certificado SSL. No he encontrado la causa real de estos errores (no es solo un certificado caducado), y obviamente es un riesgo de seguridad desactivar la validación, pero podría ser lo que necesitas para sortear los errores. Agrega estas opciones al método __doRequest() anterior:

curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

Edit2: Si obtienes un “parece que no tenemos un documento XML” SoapFault, puede ser porque el servidor está respondiendo con un documento no XML. En mi caso, la respuesta fue una página de error de autenticación HTML 401. Imprimir los objetos $request y $response en la función doRequest anterior es de gran ayuda al depurar. Resolví el error de autenticación eliminando la línea que contenía “CURLAUTH_NTLM”, así que aparentemente la autenticación NTLM no siempre se utiliza. Bueno.

Proporcionamos el nombre de usuario y la contraseña en otro envoltorio:

class ExchangeNTLMSoapClient extends NTLMSoapClient { protected $user = '[email protected]'; protected $password = 'secret'; }

Ahora podemos llamar a EWS:

$client = new ExchangeNTLMSoapClient($wsdl);

Sin embargo, esto fallará por dos razones. La primera razón es que el archivo WSDL debería contener un elemento soap:address que describa dónde encontrar la ubicación del servicio web SOAP. El archivo WSDL servido por Exchange no contiene tal elemento. Posiblemente haya otras formas de hacer esto, pero una solución es descargar el archivo WSDL y agregar lo siguiente al final:

Esto le dice a SoapClient dónde encontrar el servicio web real. Esta solución requiere que dos archivos referenciados por el archivo WSDL, types.xsd y messages.xsd, también sean descargados y colocados localmente. Esto no es un problema si solo estás contactando un servidor Exchange, pero no es una solución elegante si necesitas contactar muchos servidores.

La otra razón por la que la llamada a ExchangeNTLMSoapClient fallará es que el envoltorio solo agrega soporte NTLM a la descarga inicial del archivo WSDL. Cuando SoapClient procede a contactar el servicio web, vuelve a cambiar a la autenticación básica. Para sortear esto, creamos un nuevo objeto de flujo que utiliza CURL:

class NTLMStream { private $path; private $mode; private $options; private $opened_path; private $buffer; private $pos; public function stream_open($path, $mode, $options, $opened_path) { echo "[NTLMStream::stream_open] $path , mode=$mode n"; $this->path = $path; $this->mode = $mode; $this->options = $options; $this->opened_path = $opened_path; $this->createBuffer($path); return true; } public function stream_close() { echo "[NTLMStream::stream_close] n"; curl_close($this->ch); } public function stream_read($count) { echo "[NTLMStream::stream_read] $count n"; if(strlen($this->buffer) == 0) { return false; } $read = substr($this->buffer,$this->pos, $count); $this->pos += $count; return $read; } public function stream_write($data) { echo "[NTLMStream::stream_write] n"; if(strlen($this->buffer) == 0) { return false; } return true; } public function stream_eof() { echo "[NTLMStream::stream_eof] "; if($this->pos > strlen($this->buffer)) { echo "true n"; return true; } echo "false n"; return false; } /* devuelve la posición del puntero de lectura actual */ public function stream_tell() { echo "[NTLMStream::stream_tell] n"; return $this->pos; } public function stream_flush() { echo "[NTLMStream::stream_flush] n"; $this->buffer = null; $this->pos = null; } public function stream_stat() { echo "[NTLMStream::stream_stat] n"; $this->createBuffer($this->path); $stat = array( 'size' => strlen($this->buffer), ); return $stat; } public function url_stat($path, $flags) { echo "[NTLMStream::url_stat] n"; $this->createBuffer($path); $stat = array( 'size' => strlen($this->buffer), ); return $stat; } /* Crea el buffer solicitando la url a través de cURL */ private function createBuffer($path) { if($this->buffer) { return; } echo "[NTLMStream::createBuffer] create buffer from : $pathn"; $this->ch = curl_init($path); curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($this->ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); curl_setopt($this->ch, CURLOPT_HTTPAUTH, CURLAUTH_NTLM); curl_setopt($this->ch, CURLOPT_USERPWD, $this->user.':'.$this->password); echo $this->buffer = curl_exec($this->ch); echo "[NTLMStream::createBuffer] buffer size : ".strlen($this->buffer)."bytesn"; $this->pos = 0; } }

… y un segundo envoltorio sobre este flujo para proporcionar la contraseña para NTLMStream:

class ExchangeNTLMStream extends NTLMStream { protected $user = '[email protected]'; protected $password = 'secret'; }

Ahora necesitamos decirle a PHP que use este flujo en su lugar al llamar al servicio web:

stream_wrapper_unregister('https'); stream_wrapper_register('https', 'ExchangeNTLMStream') or die("Error al registrar el protocolo"); $wsdl = "/usr/local/www/Services.wsdl"; $client = new ExchangeNTLMSoapClient($wsdl); /* Haz algo con la conexión del servicio web */ stream_wrapper_restore('https');

Ahora tenemos una comunicación funcional con EWS. Hagamos algo con ello:

print_r($client->__getFunctions());

Esto lista las funciones disponibles. Usemos la función FindItem. Recupera todos los elementos en una carpeta específica en el servidor Exchange. Pero, ¿cómo componemos una solicitud? Mirando la lista de funciones, vemos que definen los tipos de datos del argumento y el valor de retorno. Los tipos de datos de EWS son bastante detallados y complejos, y hay más de 400 tipos de datos. Veamos cómo se ven estos tipos de datos:

print_r($client->__getTypes());

Esto describe los tipos de datos individuales en una sintaxis general similar a C.

Creamos una solicitud. La documentación de MSDN es útil para determinar los campos requeridos y sus posibles valores. Primero, enumeraremos las carpetas en el nivel superior de la cuenta:

$FindFolder->Traversal = "Shallow"; $FindFolder->FolderShape->BaseShape = "Default"; $FindFolder->ParentFolderIds->DistinguishedFolderId->Id = "root"; $result = $client->FindFolder($FindFolder); $folders = $result->ResponseMessages->FindFolderResponseMessage->RootFolder->Folders->Folder; foreach($folders as $folder) { echo $folder->DisplayName."n"; }

Ahora, busquemos todos los elementos en el calendario:

$FindItem->Traversal = "Shallow"; $FindItem->ItemShape->BaseShape = "AllProperties"; $FindItem->ParentFolderIds->DistinguishedFolderId->Id = "calendar"; $FindItem->CalendarView->StartDate = "2008-12-01T00:00:00Z"; $FindItem->CalendarView->EndDate = "2008-12-31T00:00:00Z"; $result = $client->FindItem($FindItem); $calendaritems = $result->ResponseMessages->FindItemResponseMessage->RootFolder->Items->CalendarItem; foreach($calendaritems as $item) { echo $item->Subject."n"; }

Esto nos da una lista de todos los elementos del calendario de John Doe para diciembre de 2008. Ahora eliminemos todos los elementos de esta lista. Para esto, necesitamos Id y un ChangeKey para todos los elementos:

$ids = array(); $changeKeys = array(); foreach($calendaritems as $item) { $ids[] = $item->ItemId->Id; $changeKeys[] = $item->ItemId->ChangeKey; } if(sizeof($ids) > 0) { $DeleteItem->DeleteType = "HardDelete"; $DeleteItem->SendMeetingCancellations = "SendToNone"; $DeleteItem->ItemIds->ItemId = array(); for($i = 0; $i < sizeof($ids); $i++ ) { $DeleteItem->ItemIds->ItemId[$i]->Id = $ids[$i]; $DeleteItem->ItemIds->ItemId[$i]->ChangeKey = $changeKeys[$i]; } $result = $client->DeleteItem($DeleteItem); print_r($result); }

Y finalmente, creemos un nuevo elemento en el calendario:

$CreateItem->SendMeetingInvitations = "SendToNone"; $CreateItem->SavedItemFolderId->DistinguishedFolderId->Id = "calendar"; $CreateItem->Items->CalendarItem = array(); for($i = 0; $i < 1; $i++) { $CreateItem->Items->CalendarItem[$i]->Subject = "Hola desde PHP"; $CreateItem->Items->CalendarItem[$i]->Start = "2010-01-01T16:00:00Z"; # Formato de fecha ISO. Z denota hora UTC $CreateItem->Items->CalendarItem[$i]->End = "2010-01-01T17:00:00Z"; $CreateItem->Items->CalendarItem[$i]->IsAllDayEvent = false; $CreateItem->Items->CalendarItem[$i]->LegacyFreeBusyStatus = "Busy"; $CreateItem->Items->CalendarItem[$i]->Location = "Bahamas"; $CreateItem->Items->CalendarItem[$i]->Categories->String = "MyCategory"; } $result = $client->CreateItem($CreateItem); print_r($result);

Hay muchas otras funciones disponibles y muchos otros atributos para los objetos que he utilizado en este tutorial.

Avanzado

Si necesitas extender las clases definidas en el WSDL con, por ejemplo, una función, es posible hacerlo utilizando la clase NTLMSoapClient. Agrega un constructor a la clase que registre las clases WSDL como clases PHP:

`function construct($wsdl, $options = null) { $client = new NTLMSoapClient($wsdl, $options); $types = array(); foreach($client->getTypes() as $type) {

pregmatch(“/([a-z0-9]+)s+([a-z0-9_]+([])?)(.*)?/si”, $type, $matches); $qualifier = $matches[1]; $name = $matches[2]; if($qualifier == “struct”) {

$types[$name] = $name;

if (! class_exists($name)) { eval(“class $name {}”); } else { echo “[ExchangeNTLMSoapClient::__construct] La clase $name ya existe.n”; } } }

if(is_null($options)) { $options = array(); } $options[‘classmap’] = $types; parent::__construct($wsdl, $options); }Esto carga definiciones de clases vacías para clases que no ya están definidas en el script PHP. Ahora es posible definir una clase que anule la que se carga automáticamente: >class EmailAddressDictionaryEntryType { function validate() {

return stristr(“@”, $this->Value); } }` ### Finalmente Eso es todo. Aún hay un largo camino desde este script de muestra hasta un reemplazo de Outlook, pero esto puede ser muy útil para, por ejemplo, propósitos de integración y migración de datos. Gracias a Thomas Rabaix por su artículo sobre autenticación NTLM en SOAP y PHP: http://rabaix.net/en/articles/2008/03/13/using-soap-php-with-ntlm-authentication. Gracias a Adam Delves por su artículo sobre WSDL y PHP: http://www.phpbuilder.com/columns/adam_delves20060606.php3.

Share: X/Twitter LinkedIn

Recibe nuevas publicaciones en tu bandeja de entrada.

No spam. Cancela la suscripción en cualquier momento.