SOAP Exchange · 8 min read · Dec 31, 2025

SOAP mit Exchange sprechen

SOAP mit Exchange sprechen

Früher war es nahezu unmöglich, ohne Microsoft-Produkte mit Exchange zu kommunizieren. Das binäre MAPI-Protokoll ist proprietär und schlecht dokumentiert. Exchange unterstützt IMAP und POP, aber diese Protokolle bieten nur Zugriff auf E-Mails, nicht auf den Kalender, das Adressbuch, To-Do-Listen usw. Aber beginnend mit Version 2007 wird Exchange jetzt mit einer SOAP-Schnittstelle namens Exchange Web Services oder EWS ausgeliefert. Diese Schnittstelle gibt uns Zugriff auf die Funktionen, die notwendig sind, um Clients in jeder Programmiersprache auf jeder Plattform zu schreiben.

Dieser Artikel beschreibt ein PHP-Programm, um Elemente in einem Exchange-Kalender zu suchen, zu löschen und einzufügen.

Übersicht

SOAP ist ein XML-basiertes Standardprotokoll für Webdienste. PHP unterstützt SOAP in einem separaten Modul. Ein Teil der SOAP-Spezifikation ist WSDL, eine XML-basierte Sprache zur Definition von Webdiensten, die die Datentypen und die verfügbaren Funktionen definiert. Die Funktionen und Datentypen in EWS sind tatsächlich sehr gut auf MSDN dokumentiert: http://msdn.microsoft.com/en-us/library/bb204119.aspx. EWS verwendet das HTTPS-Protokoll für die Kommunikation, aber anstelle der Basis-Authentifizierung verwendet es die Microsoft-spezifische NTLM-Authentifizierung. PHP unterstützt dieses Protokoll mit SOAP nicht, aber wie wir sehen werden, können wir einen Workaround finden.

Das Skript

Eine normale SOAP-Kommunikation in PHP sieht ungefähr so aus:

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

Auf einem Exchange 2007-Server befindet sich die WSDL-Datei normalerweise unter https://exchange.example.com/EWS/Services.wsdl. Um auf diese Datei zuzugreifen, benötigen wir einen Benutzernamen und ein Passwort für einen gültigen Benutzer auf dem Exchange-Server. Da Exchange jedoch NTLM-Authentifizierung verwendet, müssen wir einen Wrapper für SoapClient erstellen. Die CURL-Bibliothek (auch als PHP-Bibliothek verfügbar) unterstützt NTLM-Authentifizierung, also verwenden wir dies, um den Wrapper zu erstellen:

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

Diese Klasse überschreibt die doRequest-Funktion von SoapClient, um CURL zu verwenden, um die WSDL-Datei abzurufen. Je nach Ihrer PHP-Installation müssen Sie möglicherweise das PHP CURL-Modul installieren, damit dies funktioniert. Bearbeiten: Wenn Sie SoapClient-Fehler erleben, müssen Sie möglicherweise die SSL-Zertifikatsvalidierung deaktivieren. Ich habe die tatsächliche Ursache für diese Fehler nicht gefunden (es ist nicht nur ein abgelaufenes Zertifikat), und es ist offensichtlich ein Sicherheitsrisiko, die Validierung zu deaktivieren, aber es könnte notwendig sein, um die Fehler zu umgehen. Fügen Sie diese Optionen zur __doRequest()-Methode oben hinzu:

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

Bearbeiten2: Wenn Sie einen “es sieht so aus, als hätten wir kein XML-Dokument” SoapFault erhalten, kann es daran liegen, dass der Server mit einem Nicht-XML-Dokument antwortet. In meinem Fall war die Antwort eine HTML 401-Authentifizierungsfehlerseite. Das Ausdrucken der Objekte $request und $response in der doRequest-Funktion oben ist eine große Hilfe beim Debuggen. Ich habe den Authentifizierungsfehler gelöst, indem ich die Zeile mit “CURLAUTH_NTLM” gelöscht habe, also wird anscheinend die NTLM-Authentifizierung nicht immer verwendet. Na ja.

Wir geben den Benutzernamen und das Passwort in einem anderen Wrapper an:

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

Jetzt können wir EWS aufrufen:

$client = new ExchangeNTLMSoapClient($wsdl);

Dies wird jedoch aus zwei Gründen fehlschlagen. Der erste Grund ist, dass die WSDL-Datei ein soap:address-Element enthalten sollte, das beschreibt, wo sich der Standort des SOAP-Webdienstes befindet. Die von Exchange bereitgestellte WSDL-Datei enthält ein solches Element nicht. Es gibt möglicherweise andere Möglichkeiten, dies zu tun, aber eine Lösung besteht darin, die WSDL-Datei herunterzuladen und Folgendes am Ende hinzuzufügen:

Dies sagt SoapClient, wo der tatsächliche Webdienst zu finden ist. Diese Lösung erfordert, dass zwei von der WSDL-Datei referenzierte Dateien, types.xsd und messages.xsd, ebenfalls heruntergeladen und lokal platziert werden. Dies ist kein Problem, wenn Sie nur einen Exchange-Server kontaktieren, aber es ist keine elegante Lösung, wenn Sie viele Server kontaktieren müssen.

Der andere Grund, warum der Aufruf von ExchangeNTLMSoapClient fehlschlagen wird, ist, dass der Wrapper nur NTLM-Unterstützung für den anfänglichen Download der WSDL-Datei hinzufügt. Wenn SoapClient fortfährt, den Webdienst zu kontaktieren, wechselt es zurück zur Basis-Authentifizierung. Um dies zu umgehen, erstellen wir ein neues Stream-Objekt, das CURL verwendet:

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; } /* gibt die Position des aktuellen Lesezeigers zurück */ 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; } /* Erstelle den Puffer, indem du die URL über cURL anforderst */ 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; } }

… und einen zweiten Wrapper über diesen Stream, um das Passwort für NTLMStream bereitzustellen:

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

Jetzt müssen wir PHP anweisen, diesen Stream stattdessen zu verwenden, während wir den Webdienst aufrufen:

stream_wrapper_unregister('https'); stream_wrapper_register('https', 'ExchangeNTLMStream') or die("Fehler beim Registrieren des Protokolls"); $wsdl = "/usr/local/www/Services.wsdl"; $client = new ExchangeNTLMSoapClient($wsdl); /* Mach etwas mit der Webdienstverbindung */ stream_wrapper_restore('https');

Jetzt haben wir eine funktionierende Kommunikation mit EWS. Lass uns etwas damit machen:

print_r($client->__getFunctions());

Dies listet die verfügbaren Funktionen auf. Lassen Sie uns die FindItem-Funktion verwenden. Sie ruft alle Elemente in einem bestimmten Ordner auf dem Exchange-Server ab. Aber wie stellen wir eine Anfrage zusammen? Wenn wir uns die Liste der Funktionen ansehen, sehen wir, dass sie die Datentypen des Arguments und den Rückgabewert definieren. EWS-Datentypen sind ziemlich detailliert und komplex, und es gibt mehr als 400 Datentypen. Lassen Sie uns nachsehen, wie diese Datentypen aussehen:

print_r($client->__getTypes());

Dies beschreibt die einzelnen Datentypen in einer allgemeinen C-ähnlichen Syntax.

Lassen Sie uns eine Anfrage erstellen. Die MSDN-Dokumentation ist hilfreich, um erforderliche Felder und deren mögliche Werte zu bestimmen. Zuerst listen wir die Ordner auf der obersten Ebene des Kontos auf:

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

Jetzt lassen Sie uns alle Elemente im Kalender finden:

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

Dies gibt uns eine Liste aller Kalenderelemente von John Doe für Dezember 2008. Lassen Sie uns nun alle Elemente auf dieser Liste löschen. Dafür benötigen wir Id und einen ChangeKey für alle Elemente:

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

Und schließlich lassen Sie uns ein neues Element im Kalender erstellen:

$CreateItem->SendMeetingInvitations = "SendToNone"; $CreateItem->SavedItemFolderId->DistinguishedFolderId->Id = "calendar"; $CreateItem->Items->CalendarItem = array(); for($i = 0; $i < 1; $i++) { $CreateItem->Items->CalendarItem[$i]->Subject = "Hallo von PHP"; $CreateItem->Items->CalendarItem[$i]->Start = "2010-01-01T16:00:00Z"; # ISO-Datumsformat. Z steht für UTC-Zeit $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);

Es gibt viele andere Funktionen und viele andere Attribute für die Objekte, die ich in diesem Tutorial verwendet habe.

Fortgeschritten

Wenn Sie die in der WSDL definierten Klassen z.B. mit einer Funktion erweitern müssen, ist es möglich, dies mit der Klasse NTLMSoapClient zu tun. Fügen Sie einen Konstruktor zur Klasse hinzu, der die WSDL-Klassen als PHP-Klassen registriert:

`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] Klasse $name existiert bereits.n”; } } }

if(is_null($options)) { $options = array(); } $options[‘classmap’] = $types; parent::__construct($wsdl, $options); }Dies lädt leere Klassendefinitionen für Klassen, die noch nicht im PHP-Skript definiert sind. Jetzt ist es möglich, eine Klasse zu definieren, die die automatisch geladenen überschreibt: >class EmailAddressDictionaryEntryType { function validate() {

return stristr(“@”, $this->Value); } }` ### Schließlich Das ist alles. Es ist noch ein weiter Weg von diesem Beispielskript zu einem Outlook-Ersatz, aber dies kann sehr nützlich sein, z.B. für Integrationszwecke und Datenmigration. Danke an Thomas Rabaix für seinen Artikel zur NTLM-Authentifizierung in SOAP und PHP: http://rabaix.net/en/articles/2008/03/13/using-soap-php-with-ntlm-authentication. Danke an Adam Delves für seinen Artikel zu WSDL und PHP: http://www.phpbuilder.com/columns/adam_delves20060606.php3.

Share: X/Twitter LinkedIn

Erhalte neue Beiträge in deinem Posteingang.

Kein Spam. Jederzeit abmelden.