'What is the EWS SOAP XML code to get ICalUid?

We're using Office JS to develop an Addin for Outlook Calendar. For the same appointment, the EWS Ids of the Organizer and Attendees do not match. Apparently I need ICalUid from Exchange which is common across organizer and attendees and common across different events for the same Item, exactly what we need. But Office JS does not provide such property nor method to fetch it (only for .NET).

But Office JS provides Office.context.mailbox.makeEwsRequestAsync with which I can make SOAP requests to the Exchange server, using XML.

What is the EWS SOAP XML code to get ICalUid from EWS Id?

Does this work? Just replacing FieldURI with ICalUid?

function getSubjectRequest(id) {
   // Return a GetItem operation request for the subject of the specified item. 
   var result = 
'<?xml version="1.0" encoding="utf-8"?>' +
'<soap:Envelope xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"' +
'               xmlns:xsd="https://www.w3.org/2001/XMLSchema"' +
'               xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"' +
'               xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">' +
'  <soap:Header>' +
'    <RequestServerVersion Version="Exchange2013" xmlns="http://schemas.microsoft.com/exchange/services/2006/types" soap:mustUnderstand="0" />' +
'  </soap:Header>' +
'  <soap:Body>' +
'    <GetItem xmlns="http://schemas.microsoft.com/exchange/services/2006/messages">' +
'      <ItemShape>' +
'        <t:BaseShape>IdOnly</t:BaseShape>' +
'        <t:AdditionalProperties>' +
'            <t:FieldURI FieldURI="item:ICalUid"/>' +
'        </t:AdditionalProperties>' +
'      </ItemShape>' +
'      <ItemIds><t:ItemId Id="' + id + '"/></ItemIds>' +
'    </GetItem>' +
'  </soap:Body>' +
'</soap:Envelope>';

   return result;
}

function sendRequest() {
   // Create a local variable that contains the mailbox.
   var mailbox = Office.context.mailbox;

   mailbox.makeEwsRequestAsync(getSubjectRequest(mailbox.item.itemId), callback);
}

function callback(asyncResult)  {
   var result = asyncResult.value;
   var context = asyncResult.context;

   // Process the returned response here.
}


Solution 1:[1]

For the IcalUid you would need

<t:FieldURI FieldURI="calendar:UID"/>

Keep in mind through the ICalUid isn't a searchable property so you won't be able to find Appointments based on this property if you need this then look at using the Extended Goid property like João linked in the comment.

Solution 2:[2]

For the ones coming after me and for not losing 3 days on this like I did, here is how I achieved

This function gets the ewsId for both Organizer and Attendee, since they have different means to get it

// Event Outlook ID is available when
// a) Outlook event already existed in user's calendar or
// b) it was already saved by the user in the current session
function getEventOutlookUid (callback) {
  if (typeof Office.context.mailbox.item.getItemIdAsync === 'function') { // is Organizer
    Office.context.mailbox.item.getItemIdAsync(function (result) {
      if (result.status === Office.AsyncResultStatus.Succeeded) {
        callback(null, result.value)
      } else {
        console.warn(`EventOutlookUid unavailable: ${result.error.message}. Probably just a new event`)
        callback(null, null)
      }
    })
  } else if (Office.context.mailbox.item.itemId) { // is Attendee
    callback(null, Office.context.mailbox.item.itemId)
  } else {
    callback(Error('Neither Office.context.mailbox.item.getItemIdAsync nor Office.context.mailbox.item.itemId could get Outlook Item UID'))
  }
}

This function gets the extra IDs by parsing the XML from the SOAP request. You need jQuery since it is very easy to parse XML with it.

function getExtendedIds (callback) {
  getEventOutlookUid((err, eventOutlookUid) => {
    if (err) {
      console.error('Error fetching Outlook UID ' + err.message)
      callback(Error(err))
    } else {
      const soapRequest = generateCalendarUidSoapRequest(eventOutlookUid)
      if (validateXML(soapRequest)) {
        Office.context.mailbox.makeEwsRequestAsync(soapRequest, function (result) {
          if (result.status === Office.AsyncResultStatus.Succeeded) {
            // console.log(prettifyXml(result.value))
            const res = $.parseXML(result.value)

            const changeKey = res.getElementsByTagName('t:ItemId')[0].getAttribute('ChangeKey')
            const UID = res.getElementsByTagName('t:UID')[0].textContent
            const GlobalObjectId = res.getElementsByTagName('t:GlobalObjectId')[0].textContent
            const ConversationId = res.getElementsByTagName('t:ConversationId')[0].getAttribute('Id')

            callback(null, { ewsId: eventOutlookUid, changeKey, UID, GlobalObjectId, ConversationId })
          }
        })
      } else {
        callback(Error('Invalid XML request'))
      }
    }
  })
}

This function generates the XML SOAP request to get all possible ids

function generateCalendarUidSoapRequest (itemId) {
  const request = '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' +
    '  <soap:Header><t:RequestServerVersion Version="Exchange2013" /></soap:Header>' +
    '  <soap:Body>' +
    '    <m:GetItem>' +
    '      <m:ItemShape>' +
    '        <t:BaseShape>AllProperties</t:BaseShape>' +
    '      </m:ItemShape >' +
    '      <t:AdditionalProperties>' +
    '        <t:FieldURI FieldURI="calendar:UID"/>' +
    '        <t:ExtendedFieldURI DistinguishedPropertySetId="Meeting" PropertyId="3" PropertyType="Binary" />' +
    '      </t:AdditionalProperties>' +
    '      <m:ItemIds>' +
    '        <t:ItemId Id="' + itemId + '" />' +
    '      </m:ItemIds>' +
    '    </m:GetItem>' +
    '  </soap:Body>' +
    '</soap:Envelope>'

  return request
}

These are auxiliary functions to pretiffy and validate XML

function prettifyXml (sourceXml) {
  const xmlDoc = new DOMParser().parseFromString(sourceXml, 'application/xml')
  const xsltDoc = new DOMParser().parseFromString([
    // describes how we want to modify the XML - indent everything
    '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform">',
    '  <xsl:strip-space elements="*"/>',
    '  <xsl:template match="para[content-style][not(text())]">', // change to just text() to strip space in text nodes
    '    <xsl:value-of select="normalize-space(.)"/>',
    '  </xsl:template>',
    '  <xsl:template match="node()|@*">',
    '    <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>',
    '  </xsl:template>',
    '  <xsl:output indent="yes"/>',
    '</xsl:stylesheet>'
  ].join('\n'), 'application/xml')

  const xsltProcessor = new XSLTProcessor()
  xsltProcessor.importStylesheet(xsltDoc)
  const resultDoc = xsltProcessor.transformToDocument(xmlDoc)
  const resultXml = new XMLSerializer().serializeToString(resultDoc)
  return resultXml
}

function validateXML (xmlString) {
  const domParser = new DOMParser()
  const dom = domParser.parseFromString(xmlString, 'text/xml')

  // print the name of the root element or error message
  return dom.documentElement.nodeName !== 'parsererror'
}

Just add all these functions to the scope/module, and then get the extra Ids by

getExtendedIds((err, res) => {
  if (!err) {
    console.log(res)
  }
})

You'll have an object with { ewsId, changeKey, GlobalObjectId, ConversationId, UID }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 João Pimentel Ferreira
Solution 2 João Pimentel Ferreira