SOAP通信 · 3 min read · Dec 31, 2025

ExchangeとのSOAP通信

ExchangeとのSOAP通信

以前は、Microsoft製品を使用せずにExchangeと通信することはほぼ不可能でした。バイナリMAPIプロトコルは独自のもので、文書化が不十分です。ExchangeはIMAPとPOPをサポートしていますが、これらのプロトコルはメールへのアクセスのみを提供し、カレンダー、アドレス帳、TODOリストなどにはアクセスできません。しかし、2007年版から、ExchangeはExchange Web Services(EWS)と呼ばれるSOAPインターフェースを搭載しています。このインターフェースは、任意のプログラミング言語で任意のプラットフォーム上にクライアントを作成するために必要な機能へのアクセスを提供します。

この記事では、Exchangeカレンダー内のアイテムを検索、削除、挿入するPHPプログラムを説明します。

概要

SOAPは、WebサービスのためのXMLベースの標準です。PHPはSOAPを別のモジュールでサポートしています。SOAP仕様の一部はWSDLであり、これはデータ型と利用可能な関数を定義するXMLベースのWebサービス定義言語です。EWSの関数とデータ型は、実際にはMSDNで非常によく文書化されています: http://msdn.microsoft.com/en-us/library/bb204119.aspx。EWSは通信にHTTPSプロトコルを使用しますが、基本認証の代わりにMicrosoft特有のNTLM認証を使用します。PHPはSOAPでこのプロトコルをサポートしていませんが、私たちが見るように、これを回避する方法があります。

スクリプト

PHPでの通常のSOAP通信は次のようになります:

`$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 にあります。このファイルにアクセスするには、Exchangeサーバーの有効なユーザーのユーザー名とパスワードが必要です。しかし、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"; } }

このクラスは、SoapClientのdoRequest関数をオーバーライドして、CURLを使用してWSDLファイルを取得します。PHPのインストールによっては、これが機能するためにPHP CURLモジュールをインストールする必要があるかもしれません。編集: SoapClientエラーが発生した場合、SSL証明書の検証を無効にする必要があるかもしれません。これらのエラーの真の原因は見つかりませんでした(単に証明書が期限切れになっているわけではありません)、そして明らかに検証を無効にすることはセキュリティリスクですが、エラーを回避するために必要な場合があります。上記の__doRequest()メソッドにこれらのオプションを追加します:

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

編集2: “XMLドキュメントが見つかりません“というSoapFaultが発生した場合、サーバーが非XMLドキュメントで応答している可能性があります。私の場合、応答はHTML 401認証エラーページでした。上記のdoRequest関数で$requestと$responseオブジェクトを印刷することは、デバッグ時に大いに役立ちます。認証エラーは、「CURLAUTH_NTLM」を含む行を削除することで解決しました。したがって、NTLM認証は常に使用されるわけではないようです。まあ、いいでしょう。

ユーザー名とパスワードは別のラッパーで提供します:

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

これでEWSを呼び出すことができます:

$client = new ExchangeNTLMSoapClient($wsdl);

しかし、これには2つの理由で失敗します。最初の理由は、WSDLファイルにSOAP Webサービスの場所を見つけるためのsoap:address要素が含まれている必要があることです。Exchangeが提供するWSDLファイルには、そのような要素が含まれていません。これを行う他の方法もあるかもしれませんが、一つの解決策はWSDLファイルをダウンロードし、次の内容を末尾に追加することです:

これにより、SoapClientは実際のWebサービスを見つける場所を知ることができます。この解決策では、WSDLファイルによって参照される2つのファイル、types.xsdとmessages.xsdもダウンロードしてローカルに配置する必要があります。これは、1つのExchangeサーバーにのみ接続する場合には問題ありませんが、多くのサーバーに接続する必要がある場合にはエレガントな解決策ではありません。

ExchangeNTLMSoapClientへの呼び出しが失敗するもう1つの理由は、ラッパーがWSDLファイルの初回ダウンロードにのみNTLMサポートを追加することです。SoapClientがWebサービスに接続を進めると、基本認証に戻ります。これを回避するために、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; } /* cURLを介してURLを要求してバッファを作成 */ 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; } }

… そして、このストリームの上にパスワードを提供するための2番目のラッパーを作成します:

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

これで、Webサービスを呼び出す際にPHPにこのストリームを使用するように指示する必要があります:

stream_wrapper_unregister('https'); stream_wrapper_register('https', 'ExchangeNTLMStream') or die("プロトコルの登録に失敗しました"); $wsdl = "/usr/local/www/Services.wsdl"; $client = new ExchangeNTLMSoapClient($wsdl); /* Webサービス接続で何かをする */ 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年12月のJohn Doeのカレンダーアイテムのリストが取得されます。次に、このリスト内のすべてのアイテムを削除します。そのためには、すべてのアイテムの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認証に関するThomas Rabaixの素晴らしい記事に感謝します: http://rabaix.net/en/articles/2008/03/13/using-soap-php-with-ntlm-authentication。WSDLとPHPに関するAdam Delvesの素晴らしい記事にも感謝します: http://www.phpbuilder.com/columns/adam_delves20060606.php3。

Share: X/Twitter LinkedIn

新しい投稿を受信箱で受け取る

スパムはありません。いつでも購読を解除できます。