SOAP Exchange · 7 min read · Dec 31, 2025

Обсуждение SOAP с Exchange

Обсуждение SOAP с Exchange

Ранее общение с Exchange без использования продуктов Microsoft было практически невозможным. Двоичный протокол MAPI является собственным и плохо документированным. Exchange поддерживает IMAP и POP, но эти протоколы предоставляют доступ только к электронной почте, а не к календарю, адресной книге, спискам дел и т. д. Но начиная с версии 2007, Exchange теперь поставляется с интерфейсом SOAP, называемым Exchange Web Services, или EWS. Этот интерфейс предоставляет доступ к функциям, необходимым для написания клиентов на любом языке программирования на любой платформе.

Эта статья описывает программу на PHP для поиска, удаления и вставки элементов в календарь Exchange.

Обзор

SOAP — это стандарт на основе XML для веб-сервисов. PHP поддерживает SOAP в отдельном модуле. Одна часть спецификации SOAP — это WSDL, язык определения веб-сервисов на основе XML, который определяет типы данных и доступные функции. Функции и типы данных в EWS на самом деле очень хорошо документированы на MSDN: http://msdn.microsoft.com/en-us/library/bb204119.aspx. EWS использует протокол HTTPS для связи, но вместо базовой аутентификации использует специфическую для Microsoft аутентификацию NTLM. PHP не поддерживает этот протокол с SOAP, но, как мы увидим, мы можем обойти это.

Скрипт

Обычное SOAP-сообщение в PHP выглядит примерно так:

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

На сервере Exchange 2007 файл WSDL обычно находится по адресу https://exchange.example.com/EWS/Services.wsdl. Чтобы получить доступ к этому файлу, нам нужно имя пользователя и пароль для действительного пользователя на сервере Exhange. Однако, поскольку Exchange использует аутентификацию NTLM, нам нужно создать обертку для SoapClient. Библиотека CURL (также доступная как библиотека PHP) поддерживает аутентификацию NTLM, поэтому мы будем использовать это для создания обертки:

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

Этот класс переопределяет функцию doRequest класса SoapClient, чтобы использовать CURL для получения файла WSDL. В зависимости от вашей установки PHP вам может потребоваться установить модуль PHP CURL, чтобы это работало. Редактировать: Если вы сталкиваетесь с ошибками SoapClient, возможно, вам нужно отключить проверку SSL-сертификата. Я не нашел реальную причину этих ошибок (это не просто истекший сертификат), и, очевидно, это риск безопасности отключать проверку, но это может быть то, что вам нужно, чтобы обойти ошибки. Добавьте эти параметры в метод __doRequest() выше:

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

Редактировать2: Если вы получаете ошибку SoapFault “похоже, у нас нет XML-документа”, это может быть связано с тем, что сервер отвечает не-XML-документом. В моем случае ответом была HTML-страница с ошибкой аутентификации 401. Печать объектов $request и $response в функции doRequest выше очень помогает при отладке. Я решил ошибку аутентификации, удалив строку, содержащую “CURLAUTH_NTLM”, так что, похоже, аутентификация NTLM не всегда используется. Ну что ж.

Мы передаем имя пользователя и пароль в другой обертке:

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

Теперь мы можем вызывать EWS:

$client = new ExchangeNTLMSoapClient($wsdl);

Однако это не сработает по двум причинам. Первая причина заключается в том, что файл WSDL должен содержать элемент soap:address, описывающий, где найти местоположение веб-сервиса SOAP. Файл WSDL, предоставляемый Exchange, не содержит такого элемента. Возможно, есть и другие способы сделать это, но одно из решений — скачать файл WSDL и добавить следующее в конце:

Это говорит SoapClient, где найти фактический веб-сервис. Это решение требует, чтобы два файла, на которые ссылается файл WSDL, types.xsd и messages.xsd, также были загружены и размещены локально. Это не проблема, если вы обращаетесь только к одному серверу Exchange, но это не элегантное решение, если вам нужно обращаться ко многим серверам.

Другой причиной, по которой вызов ExchangeNTLMSoapClient не сработает, является то, что обертка добавляет поддержку NTLM только для первоначальной загрузки файла WSDL. Когда SoapClient начинает связываться с веб-сервисом, он переключается обратно на базовую аутентификацию. Чтобы обойти это, мы создаем новый объект потока, который использует 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; } /* возвращает позицию текущего указателя чтения */ 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; } /* Создает буфер, запрашивая URL через 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; } }

… и вторая обертка над этим потоком для передачи пароля для NTLMStream:

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

Теперь нам нужно сказать PHP использовать этот поток вместо этого при вызове веб-сервиса:

stream_wrapper_unregister('https'); stream_wrapper_register('https', 'ExchangeNTLMStream') or die("Не удалось зарегистрировать протокол"); $wsdl = "/usr/local/www/Services.wsdl"; $client = new ExchangeNTLMSoapClient($wsdl); /* Сделать что-то с соединением веб-сервиса */ stream_wrapper_restore('https');

Теперь у нас есть работающая связь с EWS. Давайте сделаем что-нибудь с этим:

print_r($client->__getFunctions());

Это перечисляет доступные функции. Давайте используем функцию FindItem. Она извлекает все элементы в конкретной папке на сервере Exchange. Но как нам составить запрос? Посмотрев на список функций, мы видим, что они определяют типы данных аргумента и возвращаемого значения. Типы данных EWS довольно детализированы и сложны, и их более 400. Давайте посмотрим, как выглядят эти типы данных:

print_r($client->__getTypes());

Это описывает отдельные типы данных в общем синтаксисе, похожем на C.

Давайте создадим запрос. Документация MSDN полезна для определения обязательных полей и их возможных значений. Сначала мы перечислим папки на верхнем уровне учетной записи:

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

Теперь давайте найдем все элементы в календаре:

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

Это дает нам список всех элементов календаря Джона Доу за декабрь 2008 года. Теперь давайте удалим все элементы из этого списка. Для этого нам нужны Id и ChangeKey для всех элементов:

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

И наконец, давайте создадим новый элемент в календаре:

$CreateItem->SendMeetingInvitations = "SendToNone"; $CreateItem->SavedItemFolderId->DistinguishedFolderId->Id = "calendar"; $CreateItem->Items->CalendarItem = array(); for($i = 0; $i < 1; $i++) { $CreateItem->Items->CalendarItem[$i]->Subject = "Hello from PHP"; $CreateItem->Items->CalendarItem[$i]->Start = "2010-01-01T16:00:00Z"; # ISO формат даты. Z обозначает время 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);

Существует множество других доступных функций и множество других атрибутов для объектов, которые я использовал в этом учебнике.

Расширенные возможности

Если вам нужно расширить классы, определенные в WSDL, например, добавить функцию, это можно сделать с помощью класса NTLMSoapClient. Добавьте конструктор в класс, который регистрирует классы WSDL как классы 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] Класс $name уже существует.n”; } } }

if(is_null($options)) { $options = array(); } $options[‘classmap’] = $types; parent::__construct($wsdl, $options); }Это загружает определения пустых классов для классов, которые еще не определены в скрипте PHP. Теперь можно определить класс, который переопределяет автоматически загруженный: >class EmailAddressDictionaryEntryType { function validate() {

return stristr(“@”, $this->Value); } }` ### В заключение На этом все. Еще далеко от этого примера скрипта до замены Outlook, но это может быть очень полезно, например, для интеграционных целей и миграции данных. Спасибо Томасу Рабайксу за его статью о аутентификации NTLM в SOAP и PHP: http://rabaix.net/en/articles/2008/03/13/using-soap-php-with-ntlm-authentication. Спасибо Адаму Делвесу за его статью о WSDL и PHP: http://www.phpbuilder.com/columns/adam_delves20060606.php3.

Share: X/Twitter LinkedIn

Get new posts in your inbox

No spam. Unsubscribe anytime.