Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ItemIds do not match between Organizer and Attendee for the same appointment/meeting #2485

Closed
jfoclpf opened this issue Mar 18, 2022 · 6 comments
Labels
Needs: triage 🔍 New issue, needs PM on rotation to triage ASAP

Comments

@jfoclpf
Copy link

jfoclpf commented Mar 18, 2022

We are developing an Addin for Outlook Calendar using Office JS, such Addin being used by both Organizers and Attendees, using the same Taskpane JS code. We need thus to obtain a unique UID for the same appointment/meeting.

As documented,

  • as Organizer using the Office.AppointmentCompose interface we use the method getItemIdAsync, i.e., Office.context.mailbox.getItemIdAsync and only after saving the Item for the ItemId to be available, and
  • as Attendee using the Office.AppointmentRead interface we use the property ItemId, that is, Office.context.mailbox.item.itemId

But they are not the same!

In the Addin JS code of the Attendee, how can I identify the ItemId of the Organizer of the appointment, in which the Attendee was invited for?

How can we have a common UID for Organizer and Attendee? Is there a EWS request I can make with Office.context.mailbox.makeEwsRequestAsync? Which XML request code then?

What I tried?

I tried a SOAP request to EWS to get the Id, but the problem persists, the Ids are different between Organizer and Attendee

var 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>IdOnly</t:BaseShape>' +
    '      </m:ItemShape >' +
    '      <m:ItemIds>' +
    '        <t:ItemId Id="' + ewsId + '" />' +
    '      </m:ItemIds>' +
    '    </m:GetItem>' +
    '  </soap:Body>' +
    '</soap:Envelope>';

Office.context.mailbox.makeEwsRequestAsync(request, function (result) {
    console.log(result.value);
});
@ghost ghost added the Needs: triage 🔍 New issue, needs PM on rotation to triage ASAP label Mar 18, 2022
@exextoc
Copy link
Collaborator

exextoc commented Mar 18, 2022

I can confirm that EWSId is not what you are looking for here. EWSId is the MAPI EntryId with stuff added to it. As a result, it is unique to each user and to each item in an Exchange Store.

Identifying an item across users looks to be a more difficult problem that has been asked by other users in the past:
https://stackoverflow.com/questions/16934908/ews-appoitnment-item-id-uniqueid-is-not-constant
https://social.msdn.microsoft.com/Forums/en-US/9d287afb-37b2-48b9-962a-6e9baf1d75a5/does-outlook-calendar-event-have-a-unique-identifier-that-can-be-used-to-identify-the-event-across
https://stackoverflow.com/questions/33692302/getting-meeting-by-icaluid-in-ews

Perhaps there is something there that can help your scenario, or you can use a combination of properties to uniquely identify an appointment.

@jfoclpf
Copy link
Author

jfoclpf commented Mar 19, 2022

Ok, thank you for your links

In the first one it is mentioned appointment.ICalUid and in the second one it is mentioned GlobalAppointmentID, both being unique for the same appointment across users. That's exactly what we need for our project.

But in the first case is for .NET and in the second case is for VBasic.

How can I fetch that in Office JS? Are there any soap XML requests? What is the XML soap code for such requests?

@jfoclpf
Copy link
Author

jfoclpf commented Mar 19, 2022

TLDR: What is the EWS SOAP XML code to get ICalUid (I can use then Office.context.mailbox.makeEwsRequestAsync)?

@jfoclpf
Copy link
Author

jfoclpf commented Mar 20, 2022

Just FYI I made a related question at stack overflow

https://stackoverflow.com/q/71539113/1243247

@davidchesnut
Copy link
Member

Hi @jfoclpf,

Looks like you are getting answers on the stackoverflow question, so I'll close this issue for now. Please reopen this issue if we missed anything.

Thanks,
David

@jfoclpf
Copy link
Author

jfoclpf commented Mar 22, 2022

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 non-saved 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 response. I use 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 }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs: triage 🔍 New issue, needs PM on rotation to triage ASAP
Projects
None yet
Development

No branches or pull requests

3 participants