SOAP Exchange · 8 min read · Dec 31, 2025

Falando sobre SOAP com Exchange

Falando sobre SOAP com Exchange

Anteriormente, conversar com o Exchange sem usar produtos da Microsoft era praticamente fora de questão. O protocolo binário MAPI é proprietário e mal documentado. O Exchange suporta IMAP e POP, mas esses protocolos apenas dão acesso a e-mails, não ao calendário, lista de contatos, listas de tarefas etc. Mas a partir da versão 2007, o Exchange agora vem com uma interface SOAP chamada Exchange Web Services, ou EWS. Esta interface nos dá acesso às funções necessárias para escrever clientes em qualquer linguagem de programação em qualquer plataforma.

Este artigo descreve um programa PHP para procurar, deletar e inserir itens em um calendário do Exchange.

Visão Geral

SOAP é um padrão baseado em XML para serviços web. O PHP suporta SOAP em um módulo separado. Uma parte da especificação SOAP é o WSDL, uma linguagem de definição de serviços web baseada em XML que define os tipos de dados e as funções disponíveis. As funções e tipos de dados no EWS estão, na verdade, muito bem documentados no MSDN: http://msdn.microsoft.com/en-us/library/bb204119.aspx. O EWS usa o protocolo HTTPS para comunicação, mas em vez de autenticação básica, usa autenticação NTLM específica da Microsoft. O PHP não suporta esse protocolo com SOAP, mas, como veremos, podemos contornar isso.

O script

Uma comunicação SOAP normal em PHP é algo assim:

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

Em um servidor Exchange 2007, o arquivo WSDL geralmente está localizado em https://exchange.example.com/EWS/Services.wsdl. Para acessar este arquivo, precisamos de um nome de usuário e senha para um usuário válido no servidor Exchange. No entanto, como o Exchange usa autenticação NTLM, precisamos criar um wrapper para o SoapClient. A biblioteca CURL (também encontrada como uma biblioteca PHP) suporta autenticação NTLM, então usaremos isso para criar o 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"; } }

Esta classe substitui a função doRequest do SoapClient para usar o CURL para buscar o arquivo WSDL. Dependendo da sua instalação do PHP, você pode precisar instalar o módulo PHP CURL para que isso funcione. Edit: Se você encontrar erros do SoapClient, pode ser necessário desabilitar a validação do certificado SSL. Não encontrei a causa real para esses erros (não é apenas um certificado expirado), e obviamente é um risco de segurança desabilitar a validação, mas pode ser o que você precisa para contornar os erros. Adicione estas opções ao método __doRequest() acima:

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

Edit2: Se você receber um “parece que não temos um documento XML” SoapFault, pode ser porque o servidor está respondendo com um documento não-XML. No meu caso, a resposta foi uma página de erro de autenticação HTML 401. Imprimir os objetos $request e $response na função doRequest acima é uma grande ajuda ao depurar. Resolvi o erro de autenticação deletando a linha que contém “CURLAUTH_NTLM”, então aparentemente a autenticação NTLM nem sempre é usada. Ah bem.

Fornecemos o nome de usuário e a senha em outro wrapper:

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

Agora podemos chamar o EWS:

$client = new ExchangeNTLMSoapClient($wsdl);

No entanto, isso falhará por duas razões. A primeira razão é que o arquivo WSDL deve conter um elemento soap:address descrevendo onde encontrar a localização do serviço web SOAP. O arquivo WSDL servido pelo Exchange não contém tal elemento. Existem possivelmente outras maneiras de fazer isso, mas uma solução é baixar o arquivo WSDL e adicionar o seguinte no final:

Isso informa ao SoapClient onde encontrar o serviço web real. Esta solução requer que dois arquivos referenciados pelo arquivo WSDL, types.xsd e messages.xsd, também sejam baixados e colocados localmente. Isso não é um problema se você estiver apenas contatando um servidor Exchange, mas não é uma solução elegante se você precisar contatar muitos servidores.

A outra razão pela qual a chamada para ExchangeNTLMSoapClient falhará é que o wrapper apenas adiciona suporte NTLM ao download inicial do arquivo WSDL. Quando o SoapClient prossegue para contatar o serviço web, ele volta a usar a autenticação básica. Para contornar isso, criamos um novo objeto de stream que usa 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; } /* retorna a posição do ponteiro de leitura atual */ 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; } /* Cria o buffer solicitando a url através do 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; } }

… e um segundo wrapper sobre este stream para fornecer a senha para NTLMStream:

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

Agora precisamos informar ao PHP para usar este stream enquanto chamamos o serviço web:

stream_wrapper_unregister('https'); stream_wrapper_register('https', 'ExchangeNTLMStream') or die("Falha ao registrar protocolo"); $wsdl = "/usr/local/www/Services.wsdl"; $client = new ExchangeNTLMSoapClient($wsdl); /* Faça algo com a conexão do serviço web */ stream_wrapper_restore('https');

Agora temos uma comunicação funcionando com o EWS. Vamos fazer algo com isso:

print_r($client->__getFunctions());

Isso lista as funções disponíveis. Vamos usar a função FindItem. Ela busca todos os itens em uma pasta específica no servidor Exchange. Mas como compomos uma solicitação? Olhando a lista de funções, vemos que elas definem os tipos de dados do argumento e o valor de retorno. Os tipos de dados do EWS são bastante detalhados e complexos, e existem mais de 400 tipos de dados. Vamos verificar como esses tipos de dados se parecem:

print_r($client->__getTypes());

Isso descreve os tipos de dados individuais em uma sintaxe geral semelhante ao C.

Vamos criar uma solicitação. A documentação do MSDN é útil para determinar os campos obrigatórios e seus possíveis valores. Primeiro, listaremos as pastas no nível superior da conta:

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

Agora, vamos encontrar todos os itens no calendário:

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

Isso nos dá uma lista de todos os itens do calendário de John Doe para dezembro de 2008. Agora vamos deletar todos os itens dessa lista. Para isso, precisamos do Id e de um ChangeKey para todos os itens:

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

E finalmente, vamos criar um novo item no calendário:

$CreateItem->SendMeetingInvitations = "SendToNone"; $CreateItem->SavedItemFolderId->DistinguishedFolderId->Id = "calendar"; $CreateItem->Items->CalendarItem = array(); for($i = 0; $i < 1; $i++) { $CreateItem->Items->CalendarItem[$i]->Subject = "Olá do PHP"; $CreateItem->Items->CalendarItem[$i]->Start = "2010-01-01T16:00:00Z"; # Formato de data ISO. Z denota hora 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);

Existem muitas outras funções disponíveis e muitos outros atributos para os objetos que usei neste tutorial.

Avançado

Se você precisar estender as classes definidas no WSDL com, por exemplo, uma função, é possível fazer isso usando a classe NTLMSoapClient. Adicione um construtor à classe que registra as classes WSDL como 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] Classe $name já existe.n”; } } }

if(is_null($options)) { $options = array(); } $options[‘classmap’] = $types; parent::__construct($wsdl, $options); }Isso carrega definições de classes vazias para classes que ainda não estão definidas no script PHP. Agora é possível definir uma classe que substitui a que foi carregada automaticamente: >class EmailAddressDictionaryEntryType { function validate() {

return stristr(“@”, $this->Value); } }` ### Finalmente É isso. Ainda há um longo caminho desde este script de exemplo até um substituto do Outlook, mas isso pode ser muito útil para, por exemplo, propósitos de integração e migração de dados. Agradecimentos a Thomas Rabaix por seu artigo sobre autenticação NTLM em SOAP e PHP: http://rabaix.net/en/articles/2008/03/13/using-soap-php-with-ntlm-authentication. Agradecimentos a Adam Delves por seu artigo sobre WSDL e PHP: http://www.phpbuilder.com/columns/adam_delves20060606.php3.

Share: X/Twitter LinkedIn

Receba novas postagens na sua caixa de entrada

Sem spam. Cancele a assinatura a qualquer momento.