1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
|
import base64
import logging
from decimal import Decimal
from .ewsdatetime import EWSDateTime
from .properties import EWSElement
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, safe_b64decode, 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' value '%s' 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' value '%s' 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 is_property_instance(cls, elem):
# Returns 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.
# TODO: Rewrite to take advantage of exchangelib.properties.ExtendedFieldURI
extended_field_uri = elem.find('{%s}ExtendedFieldURI' % TNS)
cls_props = cls.properties_map()
elem_props = {k: extended_field_uri.get(k) for k in cls_props.keys()}
# Sometimes, EWS will helpfully translate a 'distinguished_property_set_id' value to a 'property_set_id' value
# and vice versa. Align these values.
cls_set_id = cls.DISTINGUISHED_SET_NAME_TO_ID_MAP.get(cls_props.get('DistinguishedPropertySetId'))
if cls_set_id:
cls_props['PropertySetId'] = cls_set_id
else:
cls_set_name = cls.DISTINGUISHED_SET_ID_TO_NAME_MAP.get(cls_props.get('PropertySetId', ''))
if cls_set_name:
cls_props['DistinguishedPropertySetId'] = cls_set_name
elem_set_id = cls.DISTINGUISHED_SET_NAME_TO_ID_MAP.get(elem_props.get('DistinguishedPropertySetId'))
if elem_set_id:
elem_props['PropertySetId'] = elem_set_id
else:
elem_set_name = cls.DISTINGUISHED_SET_ID_TO_NAME_MAP.get(elem_props.get('PropertySetId', ''))
if elem_set_name:
elem_props['DistinguishedPropertySetId'] = elem_set_name
return cls_props == elem_props
@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)
if cls.is_binary_type():
return [safe_b64decode(val) for val in get_xml_attrs(values, '{%s}Value' % TNS)]
return [
xml_text_to_value(value=val, value_type=python_type)
for val in get_xml_attrs(values, '{%s}Value' % TNS)
]
if cls.is_binary_type():
return safe_b64decode(get_xml_attr(elem, '{%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:
if self.is_binary_type():
add_xml_child(values, 't:Value', base64.b64encode(v).decode('ascii'))
else:
add_xml_child(values, 't:Value', v)
return values
val = base64.b64encode(self.value).decode('ascii') if self.is_binary_type() else self.value
return set_xml_value(create_element('t:Value'), val, version=version)
@classmethod
def is_array_type(cls):
return cls.property_type.endswith('Array')
@classmethod
def is_binary_type(cls):
# We can't just test python_type() == bytes, because str == bytes in Python2
return 'Binary' in cls.property_type
@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 properties_map(cls):
# EWS returns PropertySetId values in lowercase in XML
return {
'DistinguishedPropertySetId': cls.distinguished_property_set_id,
'PropertySetId': cls.property_set_id.lower() if cls.property_set_id else None,
'PropertyTag': cls.property_tag_as_hex(),
'PropertyName': cls.property_name,
'PropertyId': value_to_xml_text(cls.property_id) if cls.property_id else None,
'PropertyType': 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'
__slots__ = tuple()
class Flag(ExtendedProperty):
"""This property returns 0 for Not Flagged messages, 1 for Flagged messages and 2 for Completed messages.
For a description of each status, see:
https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/flagstatus
"""
property_tag = 0x1090
property_type = 'Integer'
|