SOAP, Exchange · 6 min read · Dec 31, 2025

Exchange와 SOAP 이야기

Exchange와 SOAP 이야기

이전에는 Microsoft 제품을 사용하지 않고 Exchange와 대화하는 것은 거의 불가능했습니다. 이진 MAPI 프로토콜은 독점적이며 문서화가 잘 되어 있지 않습니다. Exchange는 IMAP과 POP을 지원하지만, 이러한 프로토콜은 이메일에만 접근할 수 있으며, 일정, 주소록, 할 일 목록 등에 접근할 수는 없습니다. 그러나 2007 버전부터 Exchange는 Exchange Web Services 또는 EWS라는 SOAP 인터페이스를 제공합니다. 이 인터페이스는 모든 플랫폼에서 모든 프로그래밍 언어로 클라이언트를 작성하는 데 필요한 기능에 접근할 수 있게 해줍니다.

이 문서에서는 Exchange 일정에서 항목을 조회, 삭제 및 삽입하는 PHP 프로그램을 설명합니다.

개요

SOAP는 웹 서비스에 대한 XML 기반 표준입니다. PHP는 별도의 모듈에서 SOAP를 지원합니다. SOAP 사양의 한 부분은 WSDL로, XML 기반의 웹 서비스 정의 언어로 데이터 유형과 사용 가능한 기능을 정의합니다. 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);

그러나 두 가지 이유로 실패할 것입니다. 첫 번째 이유는 WSDL 파일에 SOAP 웹 서비스의 위치를 설명하는 soap:address 요소가 포함되어야 하기 때문입니다. Exchange에서 제공하는 WSDL 파일에는 이러한 요소가 포함되어 있지 않습니다. 이를 수행하는 다른 방법이 있을 수 있지만, 한 가지 해결책은 WSDL 파일을 다운로드하고 다음을 추가하는 것입니다:

이것은 SoapClient에게 실제 웹 서비스를 찾는 위치를 알려줍니다. 이 솔루션은 WSDL 파일에서 참조된 두 파일인 types.xsd 및 messages.xsd도 다운로드하여 로컬에 배치해야 합니다. 하나의 Exchange 서버에만 연락하는 경우에는 문제가 되지 않지만, 여러 서버에 연락해야 하는 경우에는 우아한 솔루션이 아닙니다.

ExchangeNTLMSoapClient에 대한 호출이 실패하는 또 다른 이유는 래퍼가 WSDL 파일의 초기 다운로드에만 NTLM 지원을 추가하기 때문입니다. 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; } /* 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; } }

… 그리고 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년 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

새 게시물을 받은 편지함에서 받기

스팸은 없습니다. 언제든지 구독 해지 가능합니다.