exchangelib.extended_properties API documentation

# Module `exchangelib.extended_properties`

Expand source code

    import logging
    from decimal import Decimal

    from .ewsdatetime import EWSDateTime
    from .properties import EWSElement, ExtendedFieldURI
    from .util import create_element, add_xml_child, get_xml_attrs, get_xml_attr, set_xml_value, value_to_xml_text, \
        xml_text_to_value, is_iterable, TNS

    log = logging.getLogger(__name__)

    class ExtendedProperty(EWSElement):
        """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedproperty"""

        ELEMENT_NAME = 'ExtendedProperty'

        # Enum values: https://docs.microsoft.com/en-us/dotnet/api/exchangewebservices.distinguishedpropertysettype
        DISTINGUISHED_SETS = {
            'Address',
            'Appointment',
            'CalendarAssistant',
            'Common',
            'InternetHeaders',
            'Meeting',
            'PublicStrings',
            'Sharing',
            'Task',
            'UnifiedMessaging',
        }
        # Enum values: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedfielduri
        PROPERTY_TYPES = {
            'ApplicationTime',
            'Binary',
            'BinaryArray',
            'Boolean',
            'CLSID',
            'CLSIDArray',
            'Currency',
            'CurrencyArray',
            'Double',
            'DoubleArray',
            # 'Error',
            'Float',
            'FloatArray',
            'Integer',
            'IntegerArray',
            'Long',
            'LongArray',
            # 'Null',
            # 'Object',
            # 'ObjectArray',
            'Short',
            'ShortArray',
            'SystemTime',
            'SystemTimeArray',
            'String',
            'StringArray',
        }  # The commented-out types cannot be used for setting or getting (see docs) and are thus not very useful here

        # Translation table between common distinguished_property_set_id and property_set_id values. See
        # https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/commonly-used-property-sets
        # ID values must be lowercase.
        DISTINGUISHED_SET_NAME_TO_ID_MAP = {
            'Address': '00062004-0000-0000-c000-000000000046',
            'AirSync': '71035549-0739-4dcb-9163-00f0580dbbdf',
            'Appointment': '00062002-0000-0000-c000-000000000046',
            'Common': '00062008-0000-0000-c000-000000000046',
            'InternetHeaders': '00020386-0000-0000-c000-000000000046',
            'Log': '0006200a-0000-0000-c000-000000000046',
            'Mapi': '00020328-0000-0000-c000-000000000046',
            'Meeting': '6ed8da90-450b-101b-98da-00aa003f1305',
            'Messaging': '41f28f13-83f4-4114-a584-eedb5a6b0bff',
            'Note': '0006200e-0000-0000-c000-000000000046',
            'PostRss': '00062041-0000-0000-c000-000000000046',
            'PublicStrings': '00020329-0000-0000-c000-000000000046',
            'Remote': '00062014-0000-0000-c000-000000000046',
            'Report': '00062013-0000-0000-c000-000000000046',
            'Sharing': '00062040-0000-0000-c000-000000000046',
            'Task': '00062003-0000-0000-c000-000000000046',
            'UnifiedMessaging': '4442858e-a9e3-4e80-b900-317a210cc15b',
        }
        DISTINGUISHED_SET_ID_TO_NAME_MAP = {v: k for k, v in DISTINGUISHED_SET_NAME_TO_ID_MAP.items()}

        distinguished_property_set_id = None
        property_set_id = None
        property_tag = None  # hex integer (e.g. 0x8000) or string ('0x8000')
        property_name = None
        property_id = None  # integer as hex-formatted int (e.g. 0x8000) or normal int (32768)
        property_type = ''

        __slots__ = 'value',

        def __init__(self, *args, **kwargs):
            if not kwargs:
                # Allow to set attributes without keyword
                kwargs = dict(zip(self._slots_keys, args))
            self.value = kwargs.pop('value')
            super().__init__(**kwargs)

        @classmethod
        def validate_cls(cls):
            # Validate values of class attributes and their inter-dependencies
            cls._validate_distinguished_property_set_id()
            cls._validate_property_set_id()
            cls._validate_property_tag()
            cls._validate_property_name()
            cls._validate_property_id()
            cls._validate_property_type()

        @classmethod
        def _validate_distinguished_property_set_id(cls):
            if cls.distinguished_property_set_id:
                if any([cls.property_set_id, cls.property_tag]):
                    raise ValueError(
                        "When 'distinguished_property_set_id' is set, 'property_set_id' and 'property_tag' must be None"
                    )
                if not any([cls.property_id, cls.property_name]):
                    raise ValueError(
                        "When 'distinguished_property_set_id' is set, 'property_id' or 'property_name' must also be set"
                    )
                if cls.distinguished_property_set_id not in cls.DISTINGUISHED_SETS:
                    raise ValueError(
                        "'distinguished_property_set_id' %r must be one of %s"
                        % (cls.distinguished_property_set_id, sorted(cls.DISTINGUISHED_SETS))
                    )

        @classmethod
        def _validate_property_set_id(cls):
            if cls.property_set_id:
                if any([cls.distinguished_property_set_id, cls.property_tag]):
                    raise ValueError(
                        "When 'property_set_id' is set, 'distinguished_property_set_id' and 'property_tag' must be None"
                    )
                if not any([cls.property_id, cls.property_name]):
                    raise ValueError(
                        "When 'property_set_id' is set, 'property_id' or 'property_name' must also be set"
                    )

        @classmethod
        def _validate_property_tag(cls):
            if cls.property_tag:
                if any([
                    cls.distinguished_property_set_id, cls.property_set_id, cls.property_name, cls.property_id
                ]):
                    raise ValueError("When 'property_tag' is set, only 'property_type' must be set")
                if 0x8000 <= cls.property_tag_as_int() <= 0xFFFE:
                    raise ValueError(
                        "'property_tag' value '%s' is reserved for custom properties" % cls.property_tag_as_hex()
                    )

        @classmethod
        def _validate_property_name(cls):
            if cls.property_name:
                if any([cls.property_id, cls.property_tag]):
                    raise ValueError("When 'property_name' is set, 'property_id' and 'property_tag' must be None")
                if not any([cls.distinguished_property_set_id, cls.property_set_id]):
                    raise ValueError(
                        "When 'property_name' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set"
                    )

        @classmethod
        def _validate_property_id(cls):
            if cls.property_id:
                if any([cls.property_name, cls.property_tag]):
                    raise ValueError("When 'property_id' is set, 'property_name' and 'property_tag' must be None")
                if not any([cls.distinguished_property_set_id, cls.property_set_id]):
                    raise ValueError(
                        "When 'property_id' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set"
                    )

        @classmethod
        def _validate_property_type(cls):
            if cls.property_type not in cls.PROPERTY_TYPES:
                raise ValueError(
                    "'property_type' %r must be one of %s" % (cls.property_type, sorted(cls.PROPERTY_TYPES))
                )

        def clean(self, version=None):
            self.validate_cls()
            python_type = self.python_type()
            if self.is_array_type():
                if not is_iterable(self.value):
                    raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value))
                for v in self.value:
                    if not isinstance(v, python_type):
                        raise TypeError(
                            "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type))
            else:
                if not isinstance(self.value, python_type):
                    raise TypeError(
                        "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type))

        @classmethod
        def _normalize_obj(cls, obj):
            # Sometimes, EWS will helpfully translate a 'distinguished_property_set_id' value to a 'property_set_id' value
            # and vice versa. Align these values on an ExtendedFieldURI instance.
            try:
                obj.property_set_id = cls.DISTINGUISHED_SET_NAME_TO_ID_MAP[obj.distinguished_property_set_id]
            except KeyError:
                try:
                    obj.distinguished_property_set_id = cls.DISTINGUISHED_SET_ID_TO_NAME_MAP[obj.property_set_id]
                except KeyError:
                    pass
            return obj

        @classmethod
        def is_property_instance(cls, elem):
            """Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields
            do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a
            field in the response.
            """
            # We can't use ExtendedFieldURI.from_xml(). It clears the XML element but we may not want to consume it here.
            kwargs = {
                f.name: f.from_xml(elem=elem.find(ExtendedFieldURI.response_tag()), account=None)
                for f in ExtendedFieldURI.FIELDS
            }
            xml_obj = ExtendedFieldURI(**kwargs)
            cls_obj = cls.as_object()
            return cls._normalize_obj(cls_obj) == cls._normalize_obj(xml_obj)

        @classmethod
        def from_xml(cls, elem, account):
            # Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements
            python_type = cls.python_type()
            if cls.is_array_type():
                values = elem.find('{%s}Values' % TNS)
                return [
                    xml_text_to_value(value=val, value_type=python_type)
                    for val in get_xml_attrs(values, '{%s}Value' % TNS)
                ]
            extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type)
            if python_type == str and not extended_field_value:
                # For string types, we want to return the empty string instead of None if the element was
                # actually found, but there was no XML value. For other types, it would be more problematic
                # to make that distinction, e.g. return False for bool, 0 for int, etc.
                return ''
            return extended_field_value

        def to_xml(self, version):
            if self.is_array_type():
                values = create_element('t:Values')
                for v in self.value:
                    add_xml_child(values, 't:Value', v)
                return values
            return set_xml_value(create_element('t:Value'), self.value, version=version)

        @classmethod
        def is_array_type(cls):
            return cls.property_type.endswith('Array')

        @classmethod
        def property_tag_as_int(cls):
            if isinstance(cls.property_tag, str):
                return int(cls.property_tag, base=16)
            return cls.property_tag

        @classmethod
        def property_tag_as_hex(cls):
            return hex(cls.property_tag) if isinstance(cls.property_tag, int) else cls.property_tag

        @classmethod
        def python_type(cls):
            # Return the best equivalent for a Python type for the property type of this class
            base_type = cls.property_type[:-5] if cls.is_array_type() else cls.property_type
            return {
                'ApplicationTime': Decimal,
                'Binary': bytes,
                'Boolean': bool,
                'CLSID': str,
                'Currency': int,
                'Double': Decimal,
                'Float': Decimal,
                'Integer': int,
                'Long': int,
                'Short': int,
                'SystemTime': EWSDateTime,
                'String': str,
            }[base_type]

        @classmethod
        def as_object(cls):
            # Return an object we can use to match with the incoming object from XML
            return ExtendedFieldURI(
                distinguished_property_set_id=cls.distinguished_property_set_id,
                property_set_id=cls.property_set_id.lower() if cls.property_set_id else None,
                property_tag=cls.property_tag_as_hex(),
                property_name=cls.property_name,
                property_id=value_to_xml_text(cls.property_id) if cls.property_id else None,
                property_type=cls.property_type,
            )

    class ExternId(ExtendedProperty):
        """This is a custom extended property defined by us. It's useful for synchronization purposes, to attach a unique ID
        from an external system.
        """

        property_set_id = 'c11ff724-aa03-4555-9952-8fa248a11c3e'  # This is arbitrary. We just want a unique UUID.
        property_name = 'External ID'
        property_type = 'String'

    class Flag(ExtendedProperty):
        """This property returns None for Not Flagged messages, 1 for Completed messages and 2 for Flagged messages.

        For a description of each status, see:
        https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxoflag/eda9fd25-6407-4cec-9e62-26e4f9d6a098
        """

        property_tag = 0x1090
        property_type = 'Integer'

## Classes

`class ExtendedProperty (*args, **kwargs)`

  MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedproperty

  Expand source code

      class ExtendedProperty(EWSElement):
          """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedproperty"""

          ELEMENT_NAME = 'ExtendedProperty'

          # Enum values: https://docs.microsoft.com/en-us/dotnet/api/exchangewebservices.distinguishedpropertysettype
          DISTINGUISHED_SETS = {
              'Address',
              'Appointment',
              'CalendarAssistant',
              'Common',
              'InternetHeaders',
              'Meeting',
              'PublicStrings',
              'Sharing',
              'Task',
              'UnifiedMessaging',
          }
          # Enum values: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/extendedfielduri
          PROPERTY_TYPES = {
              'ApplicationTime',
              'Binary',
              'BinaryArray',
              'Boolean',
              'CLSID',
              'CLSIDArray',
              'Currency',
              'CurrencyArray',
              'Double',
              'DoubleArray',
              # 'Error',
              'Float',
              'FloatArray',
              'Integer',
              'IntegerArray',
              'Long',
              'LongArray',
              # 'Null',
              # 'Object',
              # 'ObjectArray',
              'Short',
              'ShortArray',
              'SystemTime',
              'SystemTimeArray',
              'String',
              'StringArray',
          }  # The commented-out types cannot be used for setting or getting (see docs) and are thus not very useful here

          # Translation table between common distinguished_property_set_id and property_set_id values. See
          # https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/commonly-used-property-sets
          # ID values must be lowercase.
          DISTINGUISHED_SET_NAME_TO_ID_MAP = {
              'Address': '00062004-0000-0000-c000-000000000046',
              'AirSync': '71035549-0739-4dcb-9163-00f0580dbbdf',
              'Appointment': '00062002-0000-0000-c000-000000000046',
              'Common': '00062008-0000-0000-c000-000000000046',
              'InternetHeaders': '00020386-0000-0000-c000-000000000046',
              'Log': '0006200a-0000-0000-c000-000000000046',
              'Mapi': '00020328-0000-0000-c000-000000000046',
              'Meeting': '6ed8da90-450b-101b-98da-00aa003f1305',
              'Messaging': '41f28f13-83f4-4114-a584-eedb5a6b0bff',
              'Note': '0006200e-0000-0000-c000-000000000046',
              'PostRss': '00062041-0000-0000-c000-000000000046',
              'PublicStrings': '00020329-0000-0000-c000-000000000046',
              'Remote': '00062014-0000-0000-c000-000000000046',
              'Report': '00062013-0000-0000-c000-000000000046',
              'Sharing': '00062040-0000-0000-c000-000000000046',
              'Task': '00062003-0000-0000-c000-000000000046',
              'UnifiedMessaging': '4442858e-a9e3-4e80-b900-317a210cc15b',
          }
          DISTINGUISHED_SET_ID_TO_NAME_MAP = {v: k for k, v in DISTINGUISHED_SET_NAME_TO_ID_MAP.items()}

          distinguished_property_set_id = None
          property_set_id = None
          property_tag = None  # hex integer (e.g. 0x8000) or string ('0x8000')
          property_name = None
          property_id = None  # integer as hex-formatted int (e.g. 0x8000) or normal int (32768)
          property_type = ''

          __slots__ = 'value',

          def __init__(self, *args, **kwargs):
              if not kwargs:
                  # Allow to set attributes without keyword
                  kwargs = dict(zip(self._slots_keys, args))
              self.value = kwargs.pop('value')
              super().__init__(**kwargs)

          @classmethod
          def validate_cls(cls):
              # Validate values of class attributes and their inter-dependencies
              cls._validate_distinguished_property_set_id()
              cls._validate_property_set_id()
              cls._validate_property_tag()
              cls._validate_property_name()
              cls._validate_property_id()
              cls._validate_property_type()

          @classmethod
          def _validate_distinguished_property_set_id(cls):
              if cls.distinguished_property_set_id:
                  if any([cls.property_set_id, cls.property_tag]):
                      raise ValueError(
                          "When 'distinguished_property_set_id' is set, 'property_set_id' and 'property_tag' must be None"
                      )
                  if not any([cls.property_id, cls.property_name]):
                      raise ValueError(
                          "When 'distinguished_property_set_id' is set, 'property_id' or 'property_name' must also be set"
                      )
                  if cls.distinguished_property_set_id not in cls.DISTINGUISHED_SETS:
                      raise ValueError(
                          "'distinguished_property_set_id' %r must be one of %s"
                          % (cls.distinguished_property_set_id, sorted(cls.DISTINGUISHED_SETS))
                      )

          @classmethod
          def _validate_property_set_id(cls):
              if cls.property_set_id:
                  if any([cls.distinguished_property_set_id, cls.property_tag]):
                      raise ValueError(
                          "When 'property_set_id' is set, 'distinguished_property_set_id' and 'property_tag' must be None"
                      )
                  if not any([cls.property_id, cls.property_name]):
                      raise ValueError(
                          "When 'property_set_id' is set, 'property_id' or 'property_name' must also be set"
                      )

          @classmethod
          def _validate_property_tag(cls):
              if cls.property_tag:
                  if any([
                      cls.distinguished_property_set_id, cls.property_set_id, cls.property_name, cls.property_id
                  ]):
                      raise ValueError("When 'property_tag' is set, only 'property_type' must be set")
                  if 0x8000 <= cls.property_tag_as_int() <= 0xFFFE:
                      raise ValueError(
                          "'property_tag' value '%s' is reserved for custom properties" % cls.property_tag_as_hex()
                      )

          @classmethod
          def _validate_property_name(cls):
              if cls.property_name:
                  if any([cls.property_id, cls.property_tag]):
                      raise ValueError("When 'property_name' is set, 'property_id' and 'property_tag' must be None")
                  if not any([cls.distinguished_property_set_id, cls.property_set_id]):
                      raise ValueError(
                          "When 'property_name' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set"
                      )

          @classmethod
          def _validate_property_id(cls):
              if cls.property_id:
                  if any([cls.property_name, cls.property_tag]):
                      raise ValueError("When 'property_id' is set, 'property_name' and 'property_tag' must be None")
                  if not any([cls.distinguished_property_set_id, cls.property_set_id]):
                      raise ValueError(
                          "When 'property_id' is set, 'distinguished_property_set_id' or 'property_set_id' must also be set"
                      )

          @classmethod
          def _validate_property_type(cls):
              if cls.property_type not in cls.PROPERTY_TYPES:
                  raise ValueError(
                      "'property_type' %r must be one of %s" % (cls.property_type, sorted(cls.PROPERTY_TYPES))
                  )

          def clean(self, version=None):
              self.validate_cls()
              python_type = self.python_type()
              if self.is_array_type():
                  if not is_iterable(self.value):
                      raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value))
                  for v in self.value:
                      if not isinstance(v, python_type):
                          raise TypeError(
                              "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type))
              else:
                  if not isinstance(self.value, python_type):
                      raise TypeError(
                          "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type))

          @classmethod
          def _normalize_obj(cls, obj):
              # Sometimes, EWS will helpfully translate a 'distinguished_property_set_id' value to a 'property_set_id' value
              # and vice versa. Align these values on an ExtendedFieldURI instance.
              try:
                  obj.property_set_id = cls.DISTINGUISHED_SET_NAME_TO_ID_MAP[obj.distinguished_property_set_id]
              except KeyError:
                  try:
                      obj.distinguished_property_set_id = cls.DISTINGUISHED_SET_ID_TO_NAME_MAP[obj.property_set_id]
                  except KeyError:
                      pass
              return obj

          @classmethod
          def is_property_instance(cls, elem):
              """Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields
              do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a
              field in the response.
              """
              # We can't use ExtendedFieldURI.from_xml(). It clears the XML element but we may not want to consume it here.
              kwargs = {
                  f.name: f.from_xml(elem=elem.find(ExtendedFieldURI.response_tag()), account=None)
                  for f in ExtendedFieldURI.FIELDS
              }
              xml_obj = ExtendedFieldURI(**kwargs)
              cls_obj = cls.as_object()
              return cls._normalize_obj(cls_obj) == cls._normalize_obj(xml_obj)

          @classmethod
          def from_xml(cls, elem, account):
              # Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements
              python_type = cls.python_type()
              if cls.is_array_type():
                  values = elem.find('{%s}Values' % TNS)
                  return [
                      xml_text_to_value(value=val, value_type=python_type)
                      for val in get_xml_attrs(values, '{%s}Value' % TNS)
                  ]
              extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type)
              if python_type == str and not extended_field_value:
                  # For string types, we want to return the empty string instead of None if the element was
                  # actually found, but there was no XML value. For other types, it would be more problematic
                  # to make that distinction, e.g. return False for bool, 0 for int, etc.
                  return ''
              return extended_field_value

          def to_xml(self, version):
              if self.is_array_type():
                  values = create_element('t:Values')
                  for v in self.value:
                      add_xml_child(values, 't:Value', v)
                  return values
              return set_xml_value(create_element('t:Value'), self.value, version=version)

          @classmethod
          def is_array_type(cls):
              return cls.property_type.endswith('Array')

          @classmethod
          def property_tag_as_int(cls):
              if isinstance(cls.property_tag, str):
                  return int(cls.property_tag, base=16)
              return cls.property_tag

          @classmethod
          def property_tag_as_hex(cls):
              return hex(cls.property_tag) if isinstance(cls.property_tag, int) else cls.property_tag

          @classmethod
          def python_type(cls):
              # Return the best equivalent for a Python type for the property type of this class
              base_type = cls.property_type[:-5] if cls.is_array_type() else cls.property_type
              return {
                  'ApplicationTime': Decimal,
                  'Binary': bytes,
                  'Boolean': bool,
                  'CLSID': str,
                  'Currency': int,
                  'Double': Decimal,
                  'Float': Decimal,
                  'Integer': int,
                  'Long': int,
                  'Short': int,
                  'SystemTime': EWSDateTime,
                  'String': str,
              }[base_type]

          @classmethod
          def as_object(cls):
              # Return an object we can use to match with the incoming object from XML
              return ExtendedFieldURI(
                  distinguished_property_set_id=cls.distinguished_property_set_id,
                  property_set_id=cls.property_set_id.lower() if cls.property_set_id else None,
                  property_tag=cls.property_tag_as_hex(),
                  property_name=cls.property_name,
                  property_id=value_to_xml_text(cls.property_id) if cls.property_id else None,
                  property_type=cls.property_type,
              )

  ### Ancestors

  - EWSElement

  ### Subclasses

  - ExternId
  - Flag

  ### Class variables

  `var DISTINGUISHED_SETS`
  `var DISTINGUISHED_SET_ID_TO_NAME_MAP`
  `var DISTINGUISHED_SET_NAME_TO_ID_MAP`
  `var ELEMENT_NAME`
  `var PROPERTY_TYPES`
  `var distinguished_property_set_id`
  `var property_id`
  `var property_name`
  `var property_set_id`
  `var property_tag`
  `var property_type`

  ### Static methods

  `def as_object()`
    Expand source code

        @classmethod
        def as_object(cls):
            # Return an object we can use to match with the incoming object from XML
            return ExtendedFieldURI(
                distinguished_property_set_id=cls.distinguished_property_set_id,
                property_set_id=cls.property_set_id.lower() if cls.property_set_id else None,
                property_tag=cls.property_tag_as_hex(),
                property_name=cls.property_name,
                property_id=value_to_xml_text(cls.property_id) if cls.property_id else None,
                property_type=cls.property_type,
            )

  `def from_xml(elem, account)`
    Expand source code

        @classmethod
        def from_xml(cls, elem, account):
            # Gets value of this specific ExtendedProperty from a list of 'ExtendedProperty' XML elements
            python_type = cls.python_type()
            if cls.is_array_type():
                values = elem.find('{%s}Values' % TNS)
                return [
                    xml_text_to_value(value=val, value_type=python_type)
                    for val in get_xml_attrs(values, '{%s}Value' % TNS)
                ]
            extended_field_value = xml_text_to_value(value=get_xml_attr(elem, '{%s}Value' % TNS), value_type=python_type)
            if python_type == str and not extended_field_value:
                # For string types, we want to return the empty string instead of None if the element was
                # actually found, but there was no XML value. For other types, it would be more problematic
                # to make that distinction, e.g. return False for bool, 0 for int, etc.
                return ''
            return extended_field_value

  `def is_array_type()`
    Expand source code

        @classmethod
        def is_array_type(cls):
            return cls.property_type.endswith('Array')

  `def is_property_instance(elem)`

    Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a field in the response.

    Expand source code

        @classmethod
        def is_property_instance(cls, elem):
            """Return whether an 'ExtendedProperty' element matches the definition for this class. Extended property fields
            do not have a name, so we must match on the cls.property_* attributes to match a field in the request with a
            field in the response.
            """
            # We can't use ExtendedFieldURI.from_xml(). It clears the XML element but we may not want to consume it here.
            kwargs = {
                f.name: f.from_xml(elem=elem.find(ExtendedFieldURI.response_tag()), account=None)
                for f in ExtendedFieldURI.FIELDS
            }
            xml_obj = ExtendedFieldURI(**kwargs)
            cls_obj = cls.as_object()
            return cls._normalize_obj(cls_obj) == cls._normalize_obj(xml_obj)

  `def property_tag_as_hex()`
    Expand source code

        @classmethod
        def property_tag_as_hex(cls):
            return hex(cls.property_tag) if isinstance(cls.property_tag, int) else cls.property_tag

  `def property_tag_as_int()`
    Expand source code

        @classmethod
        def property_tag_as_int(cls):
            if isinstance(cls.property_tag, str):
                return int(cls.property_tag, base=16)
            return cls.property_tag

  `def python_type()`
    Expand source code

        @classmethod
        def python_type(cls):
            # Return the best equivalent for a Python type for the property type of this class
            base_type = cls.property_type[:-5] if cls.is_array_type() else cls.property_type
            return {
                'ApplicationTime': Decimal,
                'Binary': bytes,
                'Boolean': bool,
                'CLSID': str,
                'Currency': int,
                'Double': Decimal,
                'Float': Decimal,
                'Integer': int,
                'Long': int,
                'Short': int,
                'SystemTime': EWSDateTime,
                'String': str,
            }[base_type]

  `def validate_cls()`
    Expand source code

        @classmethod
        def validate_cls(cls):
            # Validate values of class attributes and their inter-dependencies
            cls._validate_distinguished_property_set_id()
            cls._validate_property_set_id()
            cls._validate_property_tag()
            cls._validate_property_name()
            cls._validate_property_id()
            cls._validate_property_type()

  ### Instance variables

  `var value`

    Return an attribute of instance, which is of type owner.

  ### Methods

  `def clean(self, version=None)`
    Expand source code

        def clean(self, version=None):
            self.validate_cls()
            python_type = self.python_type()
            if self.is_array_type():
                if not is_iterable(self.value):
                    raise ValueError("'%s' value %r must be a list" % (self.__class__.__name__, self.value))
                for v in self.value:
                    if not isinstance(v, python_type):
                        raise TypeError(
                            "'%s' value element %r must be an instance of %s" % (self.__class__.__name__, v, python_type))
            else:
                if not isinstance(self.value, python_type):
                    raise TypeError(
                        "'%s' value %r must be an instance of %s" % (self.__class__.__name__, self.value, python_type))

  `def to_xml(self, version)`
    Expand source code

        def to_xml(self, version):
            if self.is_array_type():
                values = create_element('t:Values')
                for v in self.value:
                    add_xml_child(values, 't:Value', v)
                return values
            return set_xml_value(create_element('t:Value'), self.value, version=version)

  ### Inherited members

  - `EWSElement`:
    - `add_field`
    - `remove_field`
    - `supported_fields`
    - `validate_field`
`class ExternId (*args, **kwargs)`

  This is a custom extended property defined by us. It's useful for synchronization purposes, to attach a unique ID from an external system.

  Expand source code

      class ExternId(ExtendedProperty):
          """This is a custom extended property defined by us. It's useful for synchronization purposes, to attach a unique ID
          from an external system.
          """

          property_set_id = 'c11ff724-aa03-4555-9952-8fa248a11c3e'  # This is arbitrary. We just want a unique UUID.
          property_name = 'External ID'
          property_type = 'String'

  ### Ancestors

  - ExtendedProperty
  - EWSElement

  ### Class variables

  `var property_name`
  `var property_set_id`
  `var property_type`

  ### Inherited members

  - `ExtendedProperty`:
    - `add_field`
    - `is_property_instance`
    - `remove_field`
    - `supported_fields`
    - `validate_field`
    - `value`
`class Flag (*args, **kwargs)`

  This property returns None for Not Flagged messages, 1 for Completed messages and 2 for Flagged messages.

  For a description of each status, see: https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxoflag/eda9fd25-6407-4cec-9e62-26e4f9d6a098

  Expand source code

      class Flag(ExtendedProperty):
          """This property returns None for Not Flagged messages, 1 for Completed messages and 2 for Flagged messages.

          For a description of each status, see:
          https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxoflag/eda9fd25-6407-4cec-9e62-26e4f9d6a098
          """

          property_tag = 0x1090
          property_type = 'Integer'

  ### Ancestors

  - ExtendedProperty
  - EWSElement

  ### Class variables

  `var property_tag`
  `var property_type`

  ### Inherited members

  - `ExtendedProperty`:
    - `add_field`
    - `is_property_instance`
    - `remove_field`
    - `supported_fields`
    - `validate_field`
    - `value`