SOAP Exchange · 9 min read · Dec 31, 2025

Parler SOAP avec Exchange

Parler SOAP avec Exchange

Auparavant, communiquer avec Exchange sans utiliser de produits Microsoft était pratiquement hors de question. Le protocole binaire MAPI est propriétaire et mal documenté. Exchange prend en charge IMAP et POP, mais ces protocoles ne donnent accès qu’aux e-mails, pas au calendrier, au carnet d’adresses, aux listes de tâches, etc. Mais à partir de la version 2007, Exchange est désormais livré avec une interface SOAP appelée Exchange Web Services, ou EWS. Cette interface nous donne accès aux fonctions nécessaires pour écrire des clients dans n’importe quel langage de programmation sur n’importe quelle plateforme.

Cet article décrit un programme PHP pour rechercher, supprimer et insérer des éléments dans un calendrier Exchange.

Aperçu

SOAP est une norme basée sur XML pour les services web. PHP prend en charge SOAP dans un module séparé. Une partie de la spécification SOAP est WSDL, un langage de définition de service web basé sur XML qui définit les types de données et les fonctions disponibles. Les fonctions et types de données dans EWS sont en fait très bien documentés sur MSDN : http://msdn.microsoft.com/en-us/library/bb204119.aspx. EWS utilise le protocole HTTPS pour la communication, mais au lieu de l’authentification de base, il utilise l’authentification NTLM spécifique à Microsoft. PHP ne prend pas en charge ce protocole avec SOAP mais, comme nous allons le voir, nous pouvons contourner cela.

Le script

Une communication SOAP normale en PHP se déroule comme suit :

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

Sur un serveur Exchange 2007, le fichier WSDL se trouve généralement à https://exchange.example.com/EWS/Services.wsdl. Pour accéder à ce fichier, nous avons besoin d’un nom d’utilisateur et d’un mot de passe pour un utilisateur valide sur le serveur Exchange. Cependant, comme Exchange utilise l’authentification NTLM, nous devons créer un wrapper pour SoapClient. La bibliothèque CURL (également trouvée en tant que bibliothèque PHP) prend en charge l’authentification NTLM, donc nous allons l’utiliser pour créer le 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"; } }

Cette classe remplace la fonction doRequest de SoapClient pour utiliser CURL pour récupérer le fichier WSDL. Selon votre installation PHP, vous devrez peut-être installer le module PHP CURL pour que cela fonctionne. Modification : Si vous rencontrez des erreurs SoapClient, vous devrez peut-être désactiver la validation du certificat SSL. Je n’ai pas trouvé la véritable cause de ces erreurs (ce n’est pas seulement un certificat expiré), et il est évidemment risqué de désactiver la validation, mais cela pourrait être ce dont vous avez besoin pour contourner les erreurs. Ajoutez ces options à la méthode __doRequest() ci-dessus :

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

Modification2 : Si vous obtenez une erreur SoapFault “on dirait que nous n’avons pas de document XML”, cela peut être dû au fait que le serveur répond avec un document non-XML. Dans mon cas, la réponse était une page d’erreur d’authentification HTML 401. Imprimer les objets $request et $response dans la fonction doRequest ci-dessus est d’une grande aide lors du débogage. J’ai résolu l’erreur d’authentification en supprimant la ligne contenant “CURLAUTH_NTLM”, donc apparemment l’authentification NTLM n’est pas toujours utilisée. Oh bien.

Nous fournissons le nom d’utilisateur et le mot de passe dans un autre wrapper :

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

Maintenant, nous pouvons appeler EWS :

$client = new ExchangeNTLMSoapClient($wsdl);

Cependant, cela échouera pour deux raisons. La première raison est que le fichier WSDL doit contenir un élément soap:address décrivant où trouver l’emplacement du service web SOAP. Le fichier WSDL servi par Exchange ne contient pas un tel élément. Il existe peut-être d’autres moyens de faire cela, mais une solution consiste à télécharger le fichier WSDL et à ajouter ce qui suit à la fin :

Cela indique à SoapClient où trouver le véritable service web. Cette solution nécessite que deux fichiers référencés par le fichier WSDL, types.xsd et messages.xsd, soient également téléchargés et placés localement. Ce n’est pas un problème si vous ne contactez qu’un seul serveur Exchange, mais ce n’est pas une solution élégante si vous devez contacter plusieurs serveurs.

L’autre raison pour laquelle l’appel à ExchangeNTLMSoapClient échouera est que le wrapper n’ajoute le support NTLM qu’au téléchargement initial du fichier WSDL. Lorsque SoapClient procède à contacter le service web, il revient à l’authentification de base. Pour contourner cela, nous créons un nouvel objet de flux qui utilise 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; } /* retourne la position du pointeur de lecture actuel */ 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; } /* Crée le tampon en demandant l'url via 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; } }

… et un second wrapper sur ce flux pour fournir le mot de passe pour NTLMStream :

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

Maintenant, nous devons dire à PHP d’utiliser ce flux à la place lors de l’appel au service web :

stream_wrapper_unregister('https'); stream_wrapper_register('https', 'ExchangeNTLMStream') or die("Échec de l'enregistrement du protocole"); $wsdl = "/usr/local/www/Services.wsdl"; $client = new ExchangeNTLMSoapClient($wsdl); /* Faites quelque chose avec la connexion au service web */ stream_wrapper_restore('https');

Maintenant, nous avons une communication fonctionnelle avec EWS. Faisons quelque chose avec :

print_r($client->__getFunctions());

Cela liste les fonctions disponibles. Utilisons la fonction FindItem. Elle récupère tous les éléments dans un dossier spécifique sur le serveur Exchange. Mais comment composer une requête ? En regardant la liste des fonctions, nous voyons qu’elles définissent les types de données de l’argument et de la valeur de retour. Les types de données EWS sont assez détaillés et complexes, et il y a plus de 400 types de données. Voyons à quoi ressemblent ces types de données :

print_r($client->__getTypes());

Cela décrit les types de données individuels dans une syntaxe générale de type C.

Créons une requête. La documentation MSDN est utile pour déterminer les champs requis et leurs valeurs possibles. Tout d’abord, nous allons lister les dossiers au niveau supérieur du compte :

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

Maintenant, trouvons tous les éléments dans le calendrier :

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

Cela nous donne une liste de tous les éléments de calendrier de John Doe pour décembre 2008. Maintenant, supprimons tous les éléments de cette liste. Pour cela, nous avons besoin de l’Id et d’un ChangeKey pour tous les éléments :

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

Et enfin, créons un nouvel élément dans le calendrier :

$CreateItem->SendMeetingInvitations = "SendToNone"; $CreateItem->SavedItemFolderId->DistinguishedFolderId->Id = "calendar"; $CreateItem->Items->CalendarItem = array(); for($i = 0; $i < 1; $i++) { $CreateItem->Items->CalendarItem[$i]->Subject = "Bonjour de PHP"; $CreateItem->Items->CalendarItem[$i]->Start = "2010-01-01T16:00:00Z"; # Format de date ISO. Z désigne l'heure 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);

Il existe de nombreuses autres fonctions disponibles et de nombreux autres attributs pour les objets que j’ai utilisés dans ce tutoriel.

Avancé

Si vous devez étendre les classes définies dans le WSDL avec par exemple une fonction, il est possible de le faire en utilisant la classe NTLMSoapClient. Ajoutez un constructeur à la classe qui enregistre les classes WSDL en tant que classes 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 existe déjà.n”; } } }

if(is_null($options)) { $options = array(); } $options[‘classmap’] = $types; parent::__construct($wsdl, $options); }Cela charge des définitions de classes vides pour les classes qui ne sont pas déjà définies dans le script PHP. Il est maintenant possible de définir une classe qui remplace celle chargée automatiquement : >class EmailAddressDictionaryEntryType { function validate() {

return stristr(“@”, $this->Value); } }` ### Enfin C’est tout. Il reste encore un long chemin à parcourir entre ce script d’exemple et un remplacement d’Outlook, mais cela peut être très utile pour des fins d’intégration et de migration de données. Merci à Thomas Rabaix pour son article sur l’authentification NTLM dans SOAP et PHP : http://rabaix.net/en/articles/2008/03/13/using-soap-php-with-ntlm-authentication. Merci à Adam Delves pour son article sur WSDL et PHP : http://www.phpbuilder.com/columns/adam_delves20060606.php3.

Share: X/Twitter LinkedIn

Recevez de nouveaux articles dans votre boîte de réception.

Aucun spam. Désabonnez-vous à tout moment.