SOAP Exchange · 8 min read · Dec 31, 2025

Parlare di SOAP con Exchange

Parlare di SOAP con Exchange

In precedenza, parlare con Exchange senza utilizzare prodotti Microsoft era praticamente fuori discussione. Il protocollo binario MAPI è proprietario e scarsamente documentato. Exchange supporta IMAP e POP, ma questi protocolli forniscono solo accesso alle email, non al calendario, alla rubrica, alle liste di cose da fare, ecc. Ma a partire dalla versione 2007, Exchange ora include un’interfaccia SOAP chiamata Exchange Web Services, o EWS. Questa interfaccia ci dà accesso alle funzioni necessarie per scrivere client in qualsiasi linguaggio di programmazione su qualsiasi piattaforma.

Questo articolo descrive un programma PHP per cercare, eliminare e inserire elementi in un calendario di Exchange.

Panoramica

SOAP è uno standard basato su XML per i servizi web. PHP supporta SOAP in un modulo separato. Una parte della specifica SOAP è WSDL, un linguaggio di definizione dei servizi web basato su XML che definisce i tipi di dati e le funzioni disponibili. Le funzioni e i tipi di dati in EWS sono in realtà molto ben documentati su MSDN: http://msdn.microsoft.com/en-us/library/bb204119.aspx. EWS utilizza il protocollo HTTPS per la comunicazione, ma invece dell’autenticazione di base, utilizza l’autenticazione NTLM specifica di Microsoft. PHP non supporta questo protocollo con SOAP, ma, come vedremo, possiamo aggirare questo problema.

Lo script

Una normale comunicazione SOAP in PHP va più o meno così:

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

Su un server Exchange 2007, il file WSDL si trova di solito all’indirizzo https://exchange.example.com/EWS/Services.wsdl. Per accedere a questo file, abbiamo bisogno di un nome utente e di una password per un utente valido sul server Exchange. Tuttavia, poiché Exchange utilizza l’autenticazione NTLM, dobbiamo creare un wrapper per SoapClient. La libreria CURL (disponibile anche come libreria PHP) supporta l’autenticazione NTLM, quindi utilizzeremo questo per creare il wrapper:

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"; } }

Questa classe sovrascrive la funzione doRequest di SoapClient per utilizzare CURL per recuperare il file WSDL. A seconda della tua installazione PHP, potresti dover installare il modulo PHP CURL affinché questo funzioni. Modifica: Se riscontri errori di SoapClient, potresti dover disabilitare la convalida del certificato SSL. Non ho trovato la vera causa di questi errori (non è solo un certificato scaduto), e ovviamente è un rischio per la sicurezza disabilitare la convalida, ma potrebbe essere ciò di cui hai bisogno per aggirare gli errori. Aggiungi queste opzioni al metodo __doRequest() sopra:

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

Modifica2: Se ricevi un errore SoapFault “sembra che non abbiamo alcun documento XML”, potrebbe essere perché il server sta rispondendo con un documento non XML. Nel mio caso, la risposta era una pagina di errore di autenticazione HTML 401. Stampare gli oggetti $request e $response nella funzione doRequest sopra è di grande aiuto durante il debug. Ho risolto l’errore di autenticazione eliminando la riga contenente “CURLAUTH_NTLM”, quindi apparentemente l’autenticazione NTLM non viene sempre utilizzata. Oh beh.

Forniremo il nome utente e la password in un altro wrapper:

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

Ora possiamo chiamare EWS:

$client = new ExchangeNTLMSoapClient($wsdl);

Tuttavia, questo fallirà per due motivi. Il primo motivo è che il file WSDL dovrebbe contenere un elemento soap:address che descrive dove trovare la posizione del servizio web SOAP. Il file WSDL fornito da Exchange non contiene tale elemento. Ci sono possibilmente altri modi per farlo, ma una soluzione è scaricare il file WSDL e aggiungere quanto segue alla fine:

Questo dice a SoapClient dove trovare il servizio web effettivo. Questa soluzione richiede che due file referenziati dal file WSDL, types.xsd e messages.xsd, siano anche scaricati e posizionati localmente. Questo non è un problema se stai contattando solo un server Exchange, ma non è una soluzione elegante se devi contattare molti server.

L’altro motivo per cui la chiamata a ExchangeNTLMSoapClient fallirà è che il wrapper aggiunge solo supporto NTLM al download iniziale del file WSDL. Quando SoapClient procede a contattare il servizio web, torna all’autenticazione di base. Per aggirare questo, creiamo un nuovo oggetto stream che utilizza 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; } /* restituisce la posizione dell'attuale puntatore di lettura */ 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 il buffer richiedendo l'url tramite 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; } }

… e un secondo wrapper su questo stream per fornire la password per NTLMStream:

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

Ora dobbiamo dire a PHP di utilizzare questo stream mentre chiamiamo il servizio web:

stream_wrapper_unregister('https'); stream_wrapper_register('https', 'ExchangeNTLMStream') or die("Registrazione del protocollo non riuscita"); $wsdl = "/usr/local/www/Services.wsdl"; $client = new ExchangeNTLMSoapClient($wsdl); /* Fai qualcosa con la connessione al servizio web */ stream_wrapper_restore('https');

Ora abbiamo una comunicazione funzionante con EWS. Facciamo qualcosa con esso:

print_r($client->__getFunctions());

Questo elenca le funzioni disponibili. Utilizziamo la funzione FindItem. Recupera tutti gli elementi in una cartella specifica sul server Exchange. Ma come comporre una richiesta? Guardando l’elenco delle funzioni, vediamo che definiscono i tipi di dati dell’argomento e del valore di ritorno. I tipi di dati EWS sono abbastanza dettagliati e complessi, e ci sono più di 400 tipi di dati. Vediamo come sono questi tipi di dati:

print_r($client->__getTypes());

Questo descrive i singoli tipi di dati in una sintassi generale simile al C.

Creiamo una richiesta. La documentazione MSDN è utile per determinare i campi richiesti e i loro possibili valori. Prima, elenchiamo le cartelle nel livello superiore dell’account:

$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"; }

Ora, troviamo tutti gli elementi nel 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"; }

Questo ci dà un elenco di tutti gli elementi del calendario di John Doe per dicembre 2008. Ora eliminiamo tutti gli elementi di questo elenco. Per questo, abbiamo bisogno di Id e di un ChangeKey per tutti gli elementi:

$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); }

E infine, creiamo un nuovo elemento nel calendario:

$CreateItem->SendMeetingInvitations = "SendToNone"; $CreateItem->SavedItemFolderId->DistinguishedFolderId->Id = "calendar"; $CreateItem->Items->CalendarItem = array(); for($i = 0; $i < 1; $i++) { $CreateItem->Items->CalendarItem[$i]->Subject = "Ciao da PHP"; $CreateItem->Items->CalendarItem[$i]->Start = "2010-01-01T16:00:00Z"; # formato data ISO. Z denota ora 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);

Ci sono molte altre funzioni disponibili e molti altri attributi per gli oggetti che ho utilizzato in questo tutorial.

Avanzato

Se hai bisogno di estendere le classi definite nel WSDL con ad esempio una funzione, è possibile farlo utilizzando la classe NTLMSoapClient. Aggiungi un costruttore alla classe che registra le classi WSDL come classi 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 classe $name esiste già.n”; } } }

if(is_null($options)) { $options = array(); } $options[‘classmap’] = $types; parent::__construct($wsdl, $options); }Questo carica definizioni di classi vuote per classi non già definite nello script PHP. Ora è possibile definire una classe che sovrascrive quella caricata automaticamente: >class EmailAddressDictionaryEntryType { function validate() {

return stristr(“@”, $this->Value); } }` ### Infine Questo è tutto. C’è ancora molta strada da fare da questo script di esempio a un sostituto di Outlook, ma può essere molto utile per scopi di integrazione e migrazione dei dati. Grazie a Thomas Rabaix per il suo articolo sull’autenticazione NTLM in SOAP e PHP: http://rabaix.net/en/articles/2008/03/13/using-soap-php-with-ntlm-authentication. Grazie a Adam Delves per il suo articolo su WSDL e PHP: http://www.phpbuilder.com/columns/adam_delves20060606.php3.

Share: X/Twitter LinkedIn

Ricevi i nuovi post nella tua casella di posta.

Nessuno spam. Disiscriviti in qualsiasi momento.