Module exchangelib.fields
Expand source code
import abc
import datetime
import logging
from collections import OrderedDict
from decimal import Decimal, InvalidOperation
from importlib import import_module
from .ewsdatetime import EWSDateTime, EWSDate, EWSTimeZone, NaiveDateTimeNotAllowed, UnknownTimeZone, UTC
from .util import create_element, get_xml_attr, get_xml_attrs, set_xml_value, value_to_xml_text, is_iterable, \
xml_text_to_value, TNS
from .version import Build, Version, EXCHANGE_2013
log = logging.getLogger(__name__)
# DayOfWeekIndex enum. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dayofweekindex
FIRST = 'First'
SECOND = 'Second'
THIRD = 'Third'
FOURTH = 'Fourth'
LAST = 'Last'
WEEK_NUMBERS = (FIRST, SECOND, THIRD, FOURTH, LAST)
# Month enum
JANUARY = 'January'
FEBRUARY = 'February'
MARCH = 'March'
APRIL = 'April'
MAY = 'May'
JUNE = 'June'
JULY = 'July'
AUGUST = 'August'
SEPTEMBER = 'September'
OCTOBER = 'October'
NOVEMBER = 'November'
DECEMBER = 'December'
MONTHS = (JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER)
# Weekday enum
MONDAY = 'Monday'
TUESDAY = 'Tuesday'
WEDNESDAY = 'Wednesday'
THURSDAY = 'Thursday'
FRIDAY = 'Friday'
SATURDAY = 'Saturday'
SUNDAY = 'Sunday'
WEEKDAY_NAMES = (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY)
# Used for weekday recurrences except weekly recurrences. E.g. for "First WeekendDay in March"
DAY = 'Day'
WEEK_DAY = 'Weekday' # Non-weekend day
WEEKEND_DAY = 'WeekendDay'
EXTRA_WEEKDAY_OPTIONS = (DAY, WEEK_DAY, WEEKEND_DAY)
# DaysOfWeek enum: See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/daysofweek-daysofweektype
WEEKDAYS = WEEKDAY_NAMES + EXTRA_WEEKDAY_OPTIONS
class InvalidField(ValueError):
"""Used when a field name does not match any defined fields."""
class InvalidFieldForVersion(ValueError):
"""Used when a field is not supported on the given Exchnage version."""
class InvalidChoiceForVersion(ValueError):
"""Used when a value is not valid for an enum-type field."""
def split_field_path(field_path):
"""Split a string path into its field, label and subfield parts.
:param field_path:
:return Examples:
'start' -> ('start', None, None)
'phone_numbers__PrimaryPhone' -> ('phone_numbers', 'PrimaryPhone', None)
'physical_addresses__Home__street' -> ('physical_addresses', 'Home', 'street')
"""
if not isinstance(field_path, str):
raise ValueError("Field path %r must be a string" % field_path)
search_parts = field_path.split('__')
field = search_parts[0]
try:
label = search_parts[1]
except IndexError:
label = None
try:
subfield = search_parts[2]
except IndexError:
subfield = None
return field, label, subfield
def resolve_field_path(field_path, folder, strict=True):
"""Take the name of a field, or '__'-delimited path to a subfield, and return the corresponding Field object,
label and SubField object.
"""
from .indexed_properties import SingleFieldIndexedElement, MultiFieldIndexedElement
fieldname, label, subfieldname = split_field_path(field_path)
field = folder.get_item_field_by_fieldname(fieldname)
subfield = None
if isinstance(field, IndexedField):
if strict and not label:
raise ValueError(
"IndexedField path '%s' must specify label, e.g. '%s__%s'"
% (field_path, fieldname, field.value_cls.get_field_by_fieldname('label').default)
)
valid_labels = field.value_cls.get_field_by_fieldname('label').supported_choices(
version=folder.account.version
)
if label and label not in valid_labels:
raise ValueError(
"Label '%s' on IndexedField path '%s' must be one of %s"
% (label, field_path, ', '.join(valid_labels))
)
if issubclass(field.value_cls, MultiFieldIndexedElement):
if strict and not subfieldname:
raise ValueError(
"IndexedField path '%s' must specify subfield, e.g. '%s__%s__%s'"
% (field_path, fieldname, label, field.value_cls.FIELDS[1].name)
)
if subfieldname:
try:
subfield = field.value_cls.get_field_by_fieldname(subfieldname)
except ValueError:
fnames = ', '.join(f.name for f in field.value_cls.supported_fields(
version=folder.account.version
))
raise ValueError(
"Subfield '%s' on IndexedField path '%s' must be one of %s"
% (subfieldname, field_path, fnames)
)
else:
if not issubclass(field.value_cls, SingleFieldIndexedElement):
raise ValueError("'field.value_cls' %r must be an SingleFieldIndexedElement instance" % field.value_cls)
if subfieldname:
raise ValueError(
"IndexedField path '%s' must not specify subfield, e.g. just '%s__%s'"
% (field_path, fieldname, label)
)
subfield = field.value_cls.value_field(version=folder.account.version)
else:
if label or subfieldname:
raise ValueError(
"Field path '%s' must not specify label or subfield, e.g. just '%s'"
% (field_path, fieldname)
)
return field, label, subfield
class FieldPath:
"""Holds values needed to point to a single field. For indexed properties, we allow setting either field,
field and label, or field, label and subfield. This allows pointing to either the full indexed property set, a
property with a specific label, or a particular subfield field on that property.
"""
def __init__(self, field, label=None, subfield=None):
# 'label' and 'subfield' are only used for IndexedField fields
if not isinstance(field, (FieldURIField, ExtendedPropertyField)):
raise ValueError("'field' %r must be an FieldURIField, of ExtendedPropertyField instance" % field)
if label and not isinstance(label, str):
raise ValueError("'label' %r must be a %s instance" % (label, str))
if subfield and not isinstance(subfield, SubField):
raise ValueError("'subfield' %r must be a SubField instance" % subfield)
self.field = field
self.label = label
self.subfield = subfield
@classmethod
def from_string(cls, field_path, folder, strict=False):
field, label, subfield = resolve_field_path(field_path, folder=folder, strict=strict)
return cls(field=field, label=label, subfield=subfield)
def get_value(self, item):
# For indexed properties, get either the full property set, the property with matching label, or a particular
# subfield.
if self.label:
for subitem in getattr(item, self.field.name):
if subitem.label == self.label:
if self.subfield:
return getattr(subitem, self.subfield.name)
return subitem
return None # No item with this label
return getattr(item, self.field.name)
def get_sort_value(self, item):
# For fields that allow values of different types, we need to return a value that is
val = self.get_value(item)
if isinstance(self.field, DateOrDateTimeField) and isinstance(val, EWSDate):
return item.date_to_datetime(field_name=self.field.name)
return val
def to_xml(self):
if isinstance(self.field, IndexedField):
if not self.label or not self.subfield:
raise ValueError("Field path for indexed field '%s' is missing label and/or subfield" % self.field.name)
return self.subfield.field_uri_xml(field_uri=self.field.field_uri, label=self.label)
return self.field.field_uri_xml()
def expand(self, version):
# If this path does not point to a specific subfield on an indexed property, return all the possible path
# combinations for this field path.
if isinstance(self.field, IndexedField):
labels = [self.label] if self.label \
else self.field.value_cls.get_field_by_fieldname('label').supported_choices(version=version)
subfields = [self.subfield] if self.subfield else self.field.value_cls.supported_fields(version=version)
for label in labels:
for subfield in subfields:
yield FieldPath(field=self.field, label=label, subfield=subfield)
else:
yield self
@property
def path(self):
if self.label:
from .indexed_properties import SingleFieldIndexedElement
if issubclass(self.field.value_cls, SingleFieldIndexedElement) or not self.subfield:
return '%s__%s' % (self.field.name, self.label)
return '%s__%s__%s' % (self.field.name, self.label, self.subfield.name)
return self.field.name
def __eq__(self, other):
return hash(self) == hash(other)
def __str__(self):
return self.path
def __repr__(self):
return self.__class__.__name__ + repr((self.field, self.label, self.subfield))
def __hash__(self):
return hash((self.field, self.label, self.subfield))
class FieldOrder:
"""Holds values needed to call server-side sorting on a single field path."""
def __init__(self, field_path, reverse=False):
if not isinstance(field_path, FieldPath):
raise ValueError("'field_path' %r must be a FieldPath instance" % field_path)
if not isinstance(reverse, bool):
raise ValueError("'reverse' %r must be a boolean" % reverse)
self.field_path = field_path
self.reverse = reverse
@classmethod
def from_string(cls, field_path, folder):
return cls(
field_path=FieldPath.from_string(field_path=field_path.lstrip('-'), folder=folder, strict=True),
reverse=field_path.startswith('-')
)
def to_xml(self):
field_order = create_element('t:FieldOrder', attrs=dict(Order='Descending' if self.reverse else 'Ascending'))
field_order.append(self.field_path.to_xml())
return field_order
class Field(metaclass=abc.ABCMeta):
"""Holds information related to an item field."""
value_cls = None
is_list = False
# Is the field a complex EWS type? Quoting the EWS FindItem docs:
#
# The FindItem operation returns only the first 512 bytes of any streamable property. For Unicode, it returns
# the first 255 characters by using a null-terminated Unicode string. It does not return any of the message
# body formats or the recipient lists.
#
is_complex = False
def __init__(self, name=None, is_required=False, is_required_after_save=False, is_read_only=False,
is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None,
supported_from=None, deprecated_from=None):
self.name = name
self.default = default # Default value if none is given
self.is_required = is_required
# Some fields cannot be deleted on update. Default to True if 'is_required' is set
self.is_required_after_save = is_required or is_required_after_save
self.is_read_only = is_read_only
# Set this for fields that raise ErrorInvalidPropertyUpdateSentMessage on update after send. Default to True
# if 'is_read_only' is set
self.is_read_only_after_send = is_read_only or is_read_only_after_send
# Define whether the field can be used in a QuerySet. For some reason, EWS disallows searching on some fields,
# instead throwing ErrorInvalidValueForProperty
self.is_searchable = is_searchable
# When true, this field is treated as an XML attribute instead of an element
self.is_attribute = is_attribute
# The Exchange build when this field was introduced. When talking with versions prior to this version,
# we will ignore this field.
if supported_from is not None and not isinstance(supported_from, Build):
raise ValueError("'supported_from' %r must be a Build instance" % supported_from)
self.supported_from = supported_from
# The Exchange build when this field was deprecated. When talking with versions at or later than this version,
# we will ignore this field.
if deprecated_from is not None and not isinstance(deprecated_from, Build):
raise ValueError("'deprecated_from' %r must be a Build instance" % deprecated_from)
self.deprecated_from = deprecated_from
def clean(self, value, version=None):
if version and not self.supports_version(version):
raise InvalidFieldForVersion("Field '%s' does not support EWS builds prior to %s (server has %s)" % (
self.name, self.supported_from, version))
if value is None:
if self.is_required and self.default is None:
raise ValueError("'%s' is a required field with no default" % self.name)
return self.default
if self.is_list:
if not is_iterable(value):
raise ValueError("Field '%s' value %r must be a list" % (self.name, value))
for v in value:
if not isinstance(v, self.value_cls):
raise TypeError("Field '%s' value %r must be of type %s" % (self.name, v, self.value_cls))
if hasattr(v, 'clean'):
v.clean(version=version)
else:
if not isinstance(value, self.value_cls):
raise TypeError("Field '%s' value %r must be of type %s" % (self.name, value, self.value_cls))
if hasattr(value, 'clean'):
value.clean(version=version)
return value
@abc.abstractmethod
def from_xml(self, elem, account):
pass
@abc.abstractmethod
def to_xml(self, value, version):
pass
def supports_version(self, version):
# 'version' is a Version instance, for convenience by callers
if not isinstance(version, Version):
raise ValueError("'version' %r must be a Version instance" % version)
if self.supported_from and version.build < self.supported_from:
return False
if self.deprecated_from and version.build >= self.deprecated_from:
return False
return True
def __eq__(self, other):
return hash(self) == hash(other)
@abc.abstractmethod
def __hash__(self):
pass
def __repr__(self):
return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (f, getattr(self, f)) for f in (
'name', 'value_cls', 'is_list', 'is_complex', 'default'))
class FieldURIField(Field):
"""A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It
may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be
'itemtype:FieldName'
"""
def __init__(self, *args, **kwargs):
self.field_uri = kwargs.pop('field_uri', None)
self.namespace = kwargs.pop('namespace', TNS)
super().__init__(*args, **kwargs)
# See all valid FieldURI values at
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fielduri
# The field_uri has a prefix when the FieldURI points to an Item field.
if self.field_uri is None:
self.field_uri_postfix = None
elif ':' in self.field_uri:
self.field_uri_postfix = self.field_uri.split(':')[1]
else:
self.field_uri_postfix = self.field_uri
def _get_val_from_elem(self, elem):
if self.is_attribute:
return elem.get(self.field_uri) or None
return get_xml_attr(elem, self.response_tag())
def from_xml(self, elem, account):
val = self._get_val_from_elem(elem)
if val is not None:
try:
return xml_text_to_value(val, self.value_cls)
except (ValueError, InvalidOperation):
log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
return None
return self.default
def to_xml(self, value, version):
field_elem = create_element(self.request_tag())
return set_xml_value(field_elem, value, version=version)
def field_uri_xml(self):
from .properties import FieldURI
if not self.field_uri:
raise ValueError("'field_uri' value is missing")
return FieldURI(field_uri=self.field_uri).to_xml(version=None)
def request_tag(self):
if not self.field_uri_postfix:
raise ValueError("'field_uri_postfix' value is missing")
return 't:%s' % self.field_uri_postfix
def response_tag(self):
if not self.field_uri_postfix:
raise ValueError("'field_uri_postfix' value is missing")
return '{%s}%s' % (self.namespace, self.field_uri_postfix)
def __hash__(self):
return hash(self.field_uri)
class BooleanField(FieldURIField):
"""A field that handles boolean values."""
value_cls = bool
class OnOffField(BooleanField):
"""A field that handles boolean values that are On/Off instead of True/False."""
class IntegerField(FieldURIField):
"""A field that handles integer values."""
value_cls = int
def __init__(self, *args, **kwargs):
self.min = kwargs.pop('min', None)
self.max = kwargs.pop('max', None)
super().__init__(*args, **kwargs)
def _clean_single_value(self, v):
if self.min is not None and v < self.min:
raise ValueError(
"Value %r on field '%s' must be greater than %s" % (v, self.name, self.min))
if self.max is not None and v > self.max:
raise ValueError("Value %r on field '%s' must be less than %s" % (v, self.name, self.max))
def clean(self, value, version=None):
value = super().clean(value, version=version)
if value is not None:
if self.is_list:
for v in value:
self._clean_single_value(v)
else:
self._clean_single_value(value)
return value
class DecimalField(IntegerField):
"""A field that handles decimal values."""
value_cls = Decimal
class EnumField(IntegerField):
"""A field type where you can enter either the 1-based index in an enum (tuple), or the enum value. Values will be
stored internally as integers but output in XML as strings.
"""
def __init__(self, *args, **kwargs):
self.enum = kwargs.pop('enum')
# Set different min/max defaults than IntegerField
if 'max' in kwargs:
raise AttributeError("EnumField does not support the 'max' attribute")
kwargs['min'] = kwargs.pop('min', 1)
kwargs['max'] = kwargs['min'] + len(self.enum) - 1
super().__init__(*args, **kwargs)
def clean(self, value, version=None):
if self.is_list:
value = list(value) # Convert to something we can index
for i, v in enumerate(value):
if isinstance(v, str):
if v not in self.enum:
raise ValueError(
"List value '%s' on field '%s' must be one of %s" % (v, self.name, self.enum))
value[i] = self.enum.index(v) + 1
if not value:
raise ValueError("Value '%s' on field '%s' must not be empty" % (value, self.name))
if len(value) > len(set(value)):
raise ValueError("List entries '%s' on field '%s' must be unique" % (value, self.name))
else:
if isinstance(value, str):
if value not in self.enum:
raise ValueError(
"Value '%s' on field '%s' must be one of %s" % (value, self.name, self.enum))
value = self.enum.index(value) + 1
return super().clean(value, version=version)
def as_string(self, value):
# Converts an integer in the enum to its equivalent string
if isinstance(value, str):
return value
if self.is_list:
return [self.enum[v - 1] for v in sorted(value)]
return self.enum[value - 1]
def from_xml(self, elem, account):
val = self._get_val_from_elem(elem)
if val is not None:
try:
if self.is_list:
return [self.enum.index(v) + 1 for v in val.split(' ')]
return self.enum.index(val) + 1
except ValueError:
log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
return None
return self.default
def to_xml(self, value, version):
field_elem = create_element(self.request_tag())
if self.is_list:
return set_xml_value(field_elem, ' '.join(self.as_string(value)), version=version)
return set_xml_value(field_elem, self.as_string(value), version=version)
class EnumListField(EnumField):
"""Like EnumField, but for lists of enum values."""
is_list = True
class EnumAsIntField(EnumField):
"""Like EnumField, but communicates values with EWS in integers."""
def from_xml(self, elem, account):
return super(EnumField, self).from_xml(elem=elem, account=account)
def to_xml(self, value, version):
field_elem = create_element(self.request_tag())
return set_xml_value(field_elem, value, version=version)
class AppointmentStateField(IntegerField):
"""MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/appointmentstate"""
NONE = 'None'
MEETING = 'Meeting'
RECEIVED = 'Received'
CANCELLED = 'Cancelled'
STATES = {
NONE: 0x0000,
MEETING: 0x0001,
RECEIVED: 0x0002,
CANCELLED: 0x0004,
}
def from_xml(self, elem, account):
val = super().from_xml(elem=elem, account=account)
if val is None:
return val
return tuple(name for name, mask in self.STATES.items() if bool(val & mask))
class Base64Field(FieldURIField):
"""A field that handles binary data and automatically Base64 encodes and decodes the data."""
value_cls = bytes
is_complex = True
def __init__(self, *args, **kwargs):
if 'is_searchable' not in kwargs:
kwargs['is_searchable'] = False
super().__init__(*args, **kwargs)
class MimeContentField(Base64Field):
"""Like Base64Field. This element has an optional 'CharacterSet' attribute, but it specifies the encoding of the
base64-encoded string (which doesn't make sense since base64-encoded strings are always ASCII). We ignore it here
because the decoded data could be in some other encoding, specified in the "Content-Type" HTTP header.
"""
class DateField(FieldURIField):
"""A field that handles date values."""
value_cls = EWSDate
def clean(self, value, version=None):
# Allow plain datetime.date values as input
if type(value) is datetime.date:
value = self.value_cls.from_date(value)
return super().clean(value=value, version=version)
class DateTimeBackedDateField(DateField):
"""A field that acts like a date, but where values are sent to EWS as EWSDateTime."""
def __init__(self, *args, **kwargs):
# Not all fields assume a default time of 00:00, so make this configurable
self._default_time = kwargs.pop('default_time', datetime.time(0, 0))
super().__init__(*args, **kwargs)
# Create internal field to handle datetime-only logic
self._datetime_field = DateTimeField(*args, **kwargs)
def date_to_datetime(self, value):
return self._datetime_field.value_cls.combine(value, self._default_time).replace(tzinfo=UTC)
def from_xml(self, elem, account):
val = self._get_val_from_elem(elem)
if val is not None and len(val) == 25:
# This is a datetime string with timezone info, e.g. '2021-03-01T21:55:54+00:00'. We don't want to have
# datetime values converted to UTC before converting to date. EWSDateTime.from_string() insists on
# converting to UTC, but we don't have an EWSTimeZone we can convert the timezone info to. Instead, parse
# the string with .fromisoformat().
return datetime.datetime.fromisoformat(val).date()
# Revert to default parsing of datetime strings
res = self._datetime_field.from_xml(elem=elem, account=account)
if res is None:
return res
return res.date()
def to_xml(self, value, version):
# Convert date to datetime
value = self.date_to_datetime(value)
return self._datetime_field.to_xml(value=value, version=version)
class TimeField(FieldURIField):
"""A field that handles time values."""
value_cls = datetime.time
def from_xml(self, elem, account):
val = self._get_val_from_elem(elem)
if val is not None:
try:
if ':' in val:
# Assume a string of the form HH:MM:SS
return datetime.datetime.strptime(val, '%H:%M:%S').time()
# Assume an integer in minutes since midnight
return (datetime.datetime(2000, 1, 1) + datetime.timedelta(minutes=int(val))).time()
except ValueError:
pass
return self.default
class DateTimeField(FieldURIField):
"""A field that handles datetime values."""
value_cls = EWSDateTime
def clean(self, value, version=None):
if isinstance(value, datetime.datetime):
if not value.tzinfo:
raise ValueError("Value '%s' on field '%s' must be timezone aware" % (value, self.name))
if type(value) is datetime.datetime:
value = self.value_cls.from_datetime(value)
return super().clean(value, version=version)
def from_xml(self, elem, account):
val = self._get_val_from_elem(elem)
if val is not None:
try:
return self.value_cls.from_string(val)
except ValueError as e:
if isinstance(e, NaiveDateTimeNotAllowed):
# We encountered a naive datetime
if account:
# Convert to timezone-aware datetime using the default timezone of the account
tz = account.default_timezone
log.info('Found naive datetime %s on field %s. Assuming timezone %s', e.local_dt, self.name, tz)
return e.local_dt.replace(tzinfo=tz)
# There's nothing we can do but return the naive date. It's better than assuming e.g. UTC.
log.warning('Returning naive datetime %s on field %s', e.local_dt, self.name)
return e.local_dt
log.info("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
return None
return self.default
class DateOrDateTimeField(DateTimeField):
"""This field can handle both EWSDate and EWSDateTime. Used for calendar items where 'start' and 'end'
values are conceptually dates when the calendar item is an all-day event, but datetimes in all other cases, and
for recurrences where the returned 'start' and 'end' values may be either dates or datetimes depending on whether
the recurring item is a task or a calendar item.
For all-day calendar items, we assume both start and end dates are inclusive.
For filtering kwarg validation and other places where we must decide on a specific class, we settle on datetime.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Create internal field to handle date-only logic
self._date_field = DateField(*args, **kwargs)
def clean(self, value, version=None):
# Most calendar items will contain datetime values. We can't access the is_all_day value here, so CalendarItem
# must handle that sanity check.
if type(value) in (EWSDate, datetime.date):
return self._date_field.clean(value=value, version=version)
return super().clean(value=value, version=version)
def from_xml(self, elem, account):
val = self._get_val_from_elem(elem)
if val is not None and len(val) == 16:
# This is a date format with timezone info, as sent by task recurrences. Eg: '2006-01-09+01:00'
return self._date_field.from_xml(elem=elem, account=account)
return super().from_xml(elem=elem, account=account)
class TimeZoneField(FieldURIField):
"""A field that handles timezone values."""
value_cls = EWSTimeZone
def clean(self, value, version=None):
# Allow other timezone implementations as input
if value is not None:
value = self.value_cls.from_timezone(value)
return super().clean(value=value, version=version)
def from_xml(self, elem, account):
field_elem = elem.find(self.response_tag())
if field_elem is not None:
ms_id = field_elem.get('Id')
ms_name = field_elem.get('Name')
try:
return self.value_cls.from_ms_id(ms_id or ms_name)
except UnknownTimeZone:
log.warning(
"Cannot convert value '%s' on field '%s' to type %s (unknown timezone ID)",
(ms_id or ms_name), self.name, self.value_cls
)
return None
return self.default
def to_xml(self, value, version):
attrs = OrderedDict([('Id', value.ms_id)])
if value.ms_name:
attrs['Name'] = value.ms_name
return create_element(self.request_tag(), attrs=attrs)
class TextField(FieldURIField):
"""A field that stores a string value with no length limit."""
value_cls = str
is_complex = True
class TextListField(TextField):
"""Like TextField, but for lists of text."""
is_list = True
def from_xml(self, elem, account):
iter_elem = elem.find(self.response_tag())
if iter_elem is not None:
return get_xml_attrs(iter_elem, '{%s}String' % TNS)
return self.default
class MessageField(TextField):
"""A field that handles the Message element."""
INNER_ELEMENT_NAME = 'Message'
def from_xml(self, elem, account):
reply = elem.find(self.response_tag())
if reply is None:
return None
message = reply.find('{%s}%s' % (TNS, self.INNER_ELEMENT_NAME))
if message is None:
return None
return message.text
def to_xml(self, value, version):
field_elem = create_element(self.request_tag())
message = create_element('t:%s' % self.INNER_ELEMENT_NAME)
message.text = value
return set_xml_value(field_elem, message, version=version)
class CharField(TextField):
"""A field that stores a string value with a limited length."""
is_complex = False
def __init__(self, *args, **kwargs):
self.max_length = kwargs.pop('max_length', 255)
if not 1 <= self.max_length <= 255:
# A field supporting messages longer than 255 chars should be TextField
raise ValueError("'max_length' must be in the range 1-255")
super().__init__(*args, **kwargs)
def clean(self, value, version=None):
value = super().clean(value, version=version)
if value is not None:
if self.is_list:
for v in value:
if len(v) > self.max_length:
raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, v, self.max_length))
else:
if len(value) > self.max_length:
raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, value, self.max_length))
return value
class IdField(CharField):
"""A field to hold the 'Id' and 'Changekey' attributes on 'ItemId' type items. There is no guaranteed max length,
but we can assume 512 bytes in practice. See
https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/ews-identifiers-in-exchange
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.max_length = 512 # This is above the normal 255 limit, but this is actually an attribute, not a field
self.is_searchable = False
self.is_attribute = True
class CharListField(CharField):
"""Like CharField, but for lists of strings."""
is_list = True
def __init__(self, *args, **kwargs):
self.list_elem_name = kwargs.pop('list_elem_name', 'String')
super().__init__(*args, **kwargs)
def list_elem_tag(self):
return '{%s}%s' % (self.namespace, self.list_elem_name)
def from_xml(self, elem, account):
iter_elem = elem.find(self.response_tag())
if iter_elem is not None:
return get_xml_attrs(iter_elem, self.list_elem_tag())
return self.default
class URIField(TextField):
"""Helper to mark strings that must conform to xsd:anyURI.
If we want an URI validator, see http://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuri
"""
class EmailAddressField(CharField):
"""A helper class used for email address string that we can use for email validation."""
class CultureField(CharField):
"""Helper to mark strings that are # RFC 1766 culture values."""
class Choice:
"""Implement versioned choices for the ChoiceField field."""
def __init__(self, value, supported_from=None):
self.value = value
self.supported_from = supported_from
def supports_version(self, version):
# 'version' is a Version instance, for convenience by callers
if not isinstance(version, Version):
raise ValueError("'version' %r must be a Version instance" % version)
if not self.supported_from:
return True
return version.build >= self.supported_from
class ChoiceField(CharField):
"""Like CharField, but restricts the value to a limited set of strings."""
def __init__(self, *args, **kwargs):
self.choices = kwargs.pop('choices')
super().__init__(*args, **kwargs)
def clean(self, value, version=None):
value = super().clean(value, version=version)
if value is None:
return None
valid_choices = [c.value for c in self.choices]
if version:
valid_choices_for_version = self.supported_choices(version=version)
if value in valid_choices_for_version:
return value
if value in valid_choices:
raise InvalidChoiceForVersion("Choice '%s' only supports EWS builds from %s to %s (server has %s)" % (
self.name, self.supported_from or '*', self.deprecated_from or '*', version))
else:
if value in valid_choices:
return value
raise ValueError("Invalid choice '%s' for field '%s'. Valid choices are: %s" % (
value, self.name, ', '.join(valid_choices)
))
def supported_choices(self, version):
return [c.value for c in self.choices if c.supports_version(version)]
FREE_BUSY_CHOICES = [Choice('Free'), Choice('Tentative'), Choice('Busy'), Choice('OOF'), Choice('NoData'),
Choice('WorkingElsewhere', supported_from=EXCHANGE_2013)]
class FreeBusyStatusField(ChoiceField):
"""Like ChoiceField, but specifically for Free/Busy values."""
def __init__(self, *args, **kwargs):
kwargs['choices'] = set(FREE_BUSY_CHOICES)
super().__init__(*args, **kwargs)
class BodyField(TextField):
"""A TextField with specific requirements for the Item body."""
def __init__(self, *args, **kwargs):
from .properties import Body
self.value_cls = Body
super().__init__(*args, **kwargs)
def clean(self, value, version=None):
if value is not None and not isinstance(value, self.value_cls):
value = self.value_cls(value)
return super().clean(value, version=version)
def from_xml(self, elem, account):
from .properties import Body, HTMLBody
field_elem = elem.find(self.response_tag())
val = None if field_elem is None else field_elem.text or None
if val is not None:
body_type = field_elem.get('BodyType')
return {
Body.body_type: Body,
HTMLBody.body_type: HTMLBody,
}[body_type](val)
return self.default
def to_xml(self, value, version):
from .properties import Body, HTMLBody
field_elem = create_element(self.request_tag())
body_type = {
Body: Body.body_type,
HTMLBody: HTMLBody.body_type,
}[type(value)]
field_elem.set('BodyType', body_type)
return set_xml_value(field_elem, value, version=version)
class EWSElementField(FieldURIField):
"""A generic field for any EWSElement object."""
def __init__(self, *args, **kwargs):
self._value_cls = kwargs.pop('value_cls')
if 'namespace' not in kwargs:
kwargs['namespace'] = self.value_cls.NAMESPACE
super().__init__(*args, **kwargs)
@property
def value_cls(self):
if isinstance(self._value_cls, str):
# Support 'value_cls' as string to allow self-referencing classes. The class must be importable from the
# top-level module.
self._value_cls = getattr(import_module(self.__module__.split('.')[0]), self._value_cls)
return self._value_cls
def from_xml(self, elem, account):
if self.is_list:
iter_elem = elem.find(self.response_tag())
if iter_elem is not None:
return [self.value_cls.from_xml(elem=e, account=account)
for e in iter_elem.findall(self.value_cls.response_tag())]
else:
if self.field_uri is None:
sub_elem = elem.find(self.value_cls.response_tag())
else:
sub_elem = elem.find(self.response_tag())
if sub_elem is not None:
return self.value_cls.from_xml(elem=sub_elem, account=account)
return self.default
def to_xml(self, value, version):
if self.field_uri is None:
return value.to_xml(version=version)
field_elem = create_element(self.request_tag())
return set_xml_value(field_elem, value, version=version)
class EWSElementListField(EWSElementField):
"""Like EWSElementField, but for lists of EWSElement objects."""
is_list = True
is_complex = True
class AssociatedCalendarItemIdField(EWSElementField):
is_complex = True
def __init__(self, *args, **kwargs):
from .properties import AssociatedCalendarItemId
kwargs['value_cls'] = AssociatedCalendarItemId
super().__init__(*args, **kwargs)
def to_xml(self, value, version):
return value.to_xml(version=version)
class RecurrenceField(EWSElementField):
is_complex = True
def __init__(self, *args, **kwargs):
from .recurrence import Recurrence
kwargs['value_cls'] = Recurrence
super().__init__(*args, **kwargs)
def to_xml(self, value, version):
return value.to_xml(version=version)
class TaskRecurrenceField(EWSElementField):
is_complex = True
def __init__(self, *args, **kwargs):
from .recurrence import TaskRecurrence
kwargs['value_cls'] = TaskRecurrence
super().__init__(*args, **kwargs)
def to_xml(self, value, version):
return value.to_xml(version=version)
class ReferenceItemIdField(EWSElementField):
is_complex = True
def __init__(self, *args, **kwargs):
from .properties import ReferenceItemId
kwargs['value_cls'] = ReferenceItemId
super().__init__(*args, **kwargs)
def to_xml(self, value, version):
return value.to_xml(version=version)
class OccurrenceField(EWSElementField):
is_complex = True
class OccurrenceListField(OccurrenceField):
is_list = True
class MessageHeaderField(EWSElementListField):
def __init__(self, *args, **kwargs):
from .properties import MessageHeader
kwargs['value_cls'] = MessageHeader
super().__init__(*args, **kwargs)
class BaseEmailField(EWSElementField):
"""A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for."""
is_complex = True # FindItem only returns the name, not the email address
def clean(self, value, version=None):
if isinstance(value, str):
value = self.value_cls(email_address=value)
return super().clean(value, version=version)
def from_xml(self, elem, account):
if self.field_uri is None:
sub_elem = elem.find(self.value_cls.response_tag())
else:
sub_elem = elem.find(self.response_tag())
if sub_elem is not None:
if self.field_uri is not None:
# We want the nested Mailbox, not the wrapper element
nested_elem = sub_elem.find(self.value_cls.response_tag())
if nested_elem is None:
raise ValueError(
'Expected XML element %r missing on field %r' % (self.value_cls.response_tag(), self.name)
)
return self.value_cls.from_xml(elem=nested_elem, account=account)
return self.value_cls.from_xml(elem=sub_elem, account=account)
return self.default
class EmailField(BaseEmailField):
def __init__(self, *args, **kwargs):
from .properties import Email
kwargs['value_cls'] = Email
super().__init__(*args, **kwargs)
class RecipientAddressField(BaseEmailField):
def __init__(self, *args, **kwargs):
from .properties import RecipientAddress
kwargs['value_cls'] = RecipientAddress
super().__init__(*args, **kwargs)
class MailboxField(BaseEmailField):
def __init__(self, *args, **kwargs):
from .properties import Mailbox
kwargs['value_cls'] = Mailbox
super().__init__(*args, **kwargs)
class MailboxListField(EWSElementListField):
def __init__(self, *args, **kwargs):
from .properties import Mailbox
kwargs['value_cls'] = Mailbox
super().__init__(*args, **kwargs)
def clean(self, value, version=None):
if value is not None:
value = [self.value_cls(email_address=s) if isinstance(s, str) else s for s in value]
return super().clean(value, version=version)
class MemberListField(EWSElementListField):
def __init__(self, *args, **kwargs):
from .properties import Member
kwargs['value_cls'] = Member
super().__init__(*args, **kwargs)
def clean(self, value, version=None):
from .properties import Mailbox
if value is not None:
value = [
self.value_cls(mailbox=Mailbox(email_address=s)) if isinstance(s, str) else s for s in value
]
return super().clean(value, version=version)
class AttendeesField(EWSElementListField):
def __init__(self, *args, **kwargs):
from .properties import Attendee
kwargs['value_cls'] = Attendee
super().__init__(*args, **kwargs)
def clean(self, value, version=None):
from .properties import Mailbox
if value is not None:
value = [self.value_cls(mailbox=Mailbox(email_address=s), response_type='Accept')
if isinstance(s, str) else s for s in value]
return super().clean(value, version=version)
class AttachmentField(EWSElementListField):
"""A field for item attachments."""
def __init__(self, *args, **kwargs):
from .attachments import Attachment
kwargs['value_cls'] = Attachment
super().__init__(*args, **kwargs)
def from_xml(self, elem, account):
from .attachments import FileAttachment, ItemAttachment
iter_elem = elem.find(self.response_tag())
# Look for both FileAttachment and ItemAttachment
if iter_elem is not None:
attachments = []
for att_type in (ItemAttachment, FileAttachment):
attachments.extend(
[att_type.from_xml(elem=e, account=account) for e in iter_elem.findall(att_type.response_tag())]
)
return attachments
return self.default
class LabelField(ChoiceField):
"""A field to hold the label on an IndexedElement."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.is_attribute = True
def from_xml(self, elem, account):
return elem.get(self.field_uri)
class SubField(Field):
"""A field to hold the value on an IndexedElement."""
namespace = TNS
value_cls = str
def from_xml(self, elem, account):
return elem.text
def to_xml(self, value, version):
return value
@staticmethod
def field_uri_xml(field_uri, label):
from .properties import IndexedFieldURI
return IndexedFieldURI(field_uri=field_uri, field_index=label).to_xml(version=None)
def clean(self, value, version=None):
value = super().clean(value, version=version)
if self.is_required and not value:
raise ValueError('Value for subfield %r must be non-empty' % self.name)
return value
def __hash__(self):
return hash(self.name)
class EmailSubField(SubField):
"""A field to hold the value on an SingleFieldIndexedElement."""
value_cls = str
def from_xml(self, elem, account):
return elem.text or elem.get('Name') # Sometimes elem.text is empty. Exchange saves the same in 'Name' attr
class NamedSubField(SubField):
"""A field to hold the value on an MultiFieldIndexedElement."""
value_cls = str
def __init__(self, *args, **kwargs):
self.field_uri = kwargs.pop('field_uri')
if ':' in self.field_uri:
raise ValueError("'field_uri' value must not contain a colon")
super().__init__(*args, **kwargs)
def from_xml(self, elem, account):
field_elem = elem.find(self.response_tag())
val = None if field_elem is None else field_elem.text or None
if val is not None:
return val
return self.default
def to_xml(self, value, version):
field_elem = create_element(self.request_tag())
return set_xml_value(field_elem, value, version=version)
def field_uri_xml(self, field_uri, label):
from .properties import IndexedFieldURI
return IndexedFieldURI(field_uri='%s:%s' % (field_uri, self.field_uri), field_index=label).to_xml(version=None)
def request_tag(self):
return 't:%s' % self.field_uri
def response_tag(self):
return '{%s}%s' % (self.namespace, self.field_uri)
class IndexedField(EWSElementField):
"""A base class for all indexed fields."""
PARENT_ELEMENT_NAME = None
def __init__(self, *args, **kwargs):
from .indexed_properties import IndexedElement
value_cls = kwargs['value_cls']
if not issubclass(value_cls, IndexedElement):
raise ValueError("'value_cls' %r must be a subclass of IndexedElement" % value_cls)
super().__init__(*args, **kwargs)
def to_xml(self, value, version):
return set_xml_value(create_element('t:%s' % self.PARENT_ELEMENT_NAME), value, version)
def response_tag(self):
return '{%s}%s' % (self.namespace, self.PARENT_ELEMENT_NAME)
def __hash__(self):
return hash(self.field_uri)
class EmailAddressesField(IndexedField):
is_list = True
PARENT_ELEMENT_NAME = 'EmailAddresses'
def __init__(self, *args, **kwargs):
from .indexed_properties import EmailAddress
kwargs['value_cls'] = EmailAddress
super().__init__(*args, **kwargs)
def clean(self, value, version=None):
if value is not None:
default_labels = self.value_cls.LABEL_CHOICES
if len(value) > len(default_labels):
raise ValueError('This field can handle at most %s values (value: %r)' % (len(default_labels), value))
tmp = []
for s, default_label in zip(value, default_labels):
if not isinstance(s, str):
tmp.append(s)
continue
tmp.append(self.value_cls(email=s, label=default_label))
value = tmp
return super().clean(value, version=version)
class PhoneNumberField(IndexedField):
is_list = True
PARENT_ELEMENT_NAME = 'PhoneNumbers'
def __init__(self, *args, **kwargs):
from .indexed_properties import PhoneNumber
kwargs['value_cls'] = PhoneNumber
super().__init__(*args, **kwargs)
class PhysicalAddressField(IndexedField):
is_list = True
PARENT_ELEMENT_NAME = 'PhysicalAddresses'
def __init__(self, *args, **kwargs):
from .indexed_properties import PhysicalAddress
kwargs['value_cls'] = PhysicalAddress
super().__init__(*args, **kwargs)
class ExtendedPropertyField(Field):
def __init__(self, *args, **kwargs):
self.value_cls = kwargs.pop('value_cls')
super().__init__(*args, **kwargs)
def clean(self, value, version=None):
if value is None:
if self.is_required:
raise ValueError("'%s' is a required field" % self.name)
return self.default
if not isinstance(value, self.value_cls):
# Allow keeping ExtendedProperty field values as their simple Python type, but run clean() anyway
tmp = self.value_cls(value)
tmp.clean(version=version)
return value
value.clean(version=version)
return value
def field_uri_xml(self):
from .properties import ExtendedFieldURI
cls = self.value_cls
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,
).to_xml(version=None)
def from_xml(self, elem, account):
extended_properties = elem.findall(self.value_cls.response_tag())
for extended_property in extended_properties:
if self.value_cls.is_property_instance(extended_property):
return self.value_cls.from_xml(elem=extended_property, account=account)
return self.default
def to_xml(self, value, version):
extended_property = create_element(self.value_cls.request_tag())
set_xml_value(extended_property, self.field_uri_xml(), version=version)
if isinstance(value, self.value_cls):
set_xml_value(extended_property, value, version=version)
else:
# Allow keeping ExtendedProperty field values as their simple Python type
set_xml_value(extended_property, self.value_cls(value), version=version)
return extended_property
def __hash__(self):
return hash(self.name)
class ItemField(FieldURIField):
@property
def value_cls(self):
from .items import Item
return Item
def from_xml(self, elem, account):
from .items import ITEM_CLASSES
for item_cls in ITEM_CLASSES:
item_elem = elem.find(item_cls.response_tag())
if item_elem is not None:
return item_cls.from_xml(elem=item_elem, account=account)
return None
def to_xml(self, value, version):
# We don't want to wrap in an Item element
return value.to_xml(version=version)
class UnknownEntriesField(CharListField):
def list_elem_tag(self):
return '{%s}UnknownEntry' % self.namespace
class PermissionSetField(EWSElementField):
is_complex = True
def __init__(self, *args, **kwargs):
from .properties import PermissionSet
kwargs['value_cls'] = PermissionSet
super().__init__(*args, **kwargs)
class EffectiveRightsField(EWSElementField):
def __init__(self, *args, **kwargs):
from .properties import EffectiveRights
kwargs['value_cls'] = EffectiveRights
super().__init__(*args, **kwargs)
class BuildField(CharField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.value_cls = Build
def from_xml(self, elem, account):
val = self._get_val_from_elem(elem)
if val:
try:
return self.value_cls.from_hex_string(val)
except (TypeError, ValueError):
log.warning('Invalid server version string: %r', val)
return val
class ProtocolListField(EWSElementListField):
# There is not containing element for this field. Just multiple 'Protocol' elements on the 'Account' element.
def __init__(self, *args, **kwargs):
from .autodiscover.properties import Protocol
kwargs['value_cls'] = Protocol
super().__init__(*args, **kwargs)
def from_xml(self, elem, account):
return [self.value_cls.from_xml(elem=e, account=account) for e in elem.findall(self.value_cls.response_tag())]
class RoutingTypeField(ChoiceField):
def __init__(self, *args, **kwargs):
kwargs['choices'] = {Choice('SMTP'), Choice('EX')}
kwargs['default'] = 'SMTP'
super().__init__(*args, **kwargs)
class IdElementField(EWSElementField):
def __init__(self, *args, **kwargs):
kwargs['is_searchable'] = False
kwargs['is_read_only'] = True
super().__init__(*args, **kwargs)
class TypeValueField(FieldURIField):
"""This field type has no value_cls because values may have many different types."""
TYPES_MAP = {
'Boolean': bool,
'Integer32': int,
'UnsignedInteger32': int,
'Integer64': int,
'UnsignedInteger64': int,
# Python doesn't have a single-byte type to represent 'Byte'
'ByteArray': bytes,
'String': str,
'StringArray': str, # A list of strings
'DateTime': EWSDateTime,
}
TYPES_MAP_REVERSED = {
bool: 'Boolean',
int: 'Integer64',
# Python doesn't have a single-byte type to represent 'Byte'
bytes: 'ByteArray',
str: 'String',
datetime.datetime: 'DateTime',
EWSDateTime: 'DateTime',
}
@classmethod
def get_type(cls, value):
if isinstance(value, bytes) and len(value) == 1:
# This is a single byte. Translate it to the 'Byte' type
return 'Byte'
if is_iterable(value):
# We don't allow generators as values, so keep the logic simple
try:
first = next(iter(value))
except StopIteration:
first = None
value_type = '%sArray' % cls.TYPES_MAP_REVERSED[type(first)]
if value_type not in cls.TYPES_MAP:
raise ValueError('%r is not a supported type' % value)
return value_type
return cls.TYPES_MAP_REVERSED[type(value)]
@classmethod
def is_array_type(cls, value_type):
return value_type == 'StringArray'
def clean(self, value, version=None):
if value is None:
if self.is_required and self.default is None:
raise ValueError("'%s' is a required field with no default" % self.name)
return self.default
return value
def from_xml(self, elem, account):
field_elem = elem.find(self.response_tag())
if field_elem is None:
return self.default
value_type_str = get_xml_attr(field_elem, '{%s}Type' % TNS)
value = get_xml_attr(field_elem, '{%s}Value' % TNS)
if value_type_str == 'Byte':
try:
# The value is an unsigned integer in the range 0 -> 255. Convert it to a single byte
return xml_text_to_value(value, int).to_bytes(1, 'little', signed=False)
except OverflowError as e:
log.warning('Invalid byte value %r (%e)', value, e)
return None
value_type = self.TYPES_MAP[value_type_str]
if self. is_array_type(value_type_str):
return tuple(xml_text_to_value(value=v, value_type=value_type) for v in value.split(' '))
return xml_text_to_value(value=value, value_type=value_type)
def to_xml(self, value, version):
value_type_str = self.get_type(value)
if value_type_str == 'Byte':
# A single byte is encoded to an unsigned integer in the range 0 -> 255
value = int.from_bytes(value, byteorder='little', signed=False)
elif is_iterable(value):
value = ' '.join(value_to_xml_text(v) for v in value)
field_elem = create_element(self.request_tag())
field_elem.append(set_xml_value(create_element('t:Type'), value_type_str, version=version))
field_elem.append(set_xml_value(create_element('t:Value'), value, version=version))
return field_elem
class DictionaryField(FieldURIField):
value_cls = dict
def from_xml(self, elem, account):
from .properties import DictionaryEntry
iter_elem = elem.find(self.response_tag())
if iter_elem is not None:
entries = [
DictionaryEntry.from_xml(elem=e, account=account)
for e in iter_elem.findall(DictionaryEntry.response_tag())
]
return {e.key: e.value for e in entries}
return self.default
def clean(self, value, version=None):
if isinstance(value, dict):
cleaned = {}
for k, v in value.items():
if type(k) is datetime.datetime:
k = EWSDateTime.from_datetime(k)
if type(v) is datetime.datetime:
v = EWSDateTime.from_datetime(v)
cleaned[k] = v
value = cleaned
return super().clean(value=value, version=version)
def to_xml(self, value, version):
from .properties import DictionaryEntry
field_elem = create_element(self.request_tag())
entries = [DictionaryEntry(key=k, value=v) for k, v in value.items()]
return set_xml_value(field_elem, entries, version=version)
class PersonaPhoneNumberField(EWSElementField):
is_complex = True
def __init__(self, *args, **kwargs):
from .properties import PhoneNumber
kwargs['value_cls'] = PhoneNumber
super().__init__(*args, **kwargs)
class BodyContentAttributedValueField(EWSElementField):
is_complex = True
def __init__(self, *args, **kwargs):
from .properties import BodyContentAttributedValue
kwargs['value_cls'] = BodyContentAttributedValue
super().__init__(*args, **kwargs)
class StringAttributedValueField(EWSElementListField):
def __init__(self, *args, **kwargs):
from .properties import StringAttributedValue
kwargs['value_cls'] = StringAttributedValue
super().__init__(*args, **kwargs)
class PhoneNumberAttributedValueField(EWSElementListField):
def __init__(self, *args, **kwargs):
from .properties import PhoneNumberAttributedValue
kwargs['value_cls'] = PhoneNumberAttributedValue
super().__init__(*args, **kwargs)
class EmailAddressAttributedValueField(EWSElementListField):
def __init__(self, *args, **kwargs):
from .properties import EmailAddressAttributedValue
kwargs['value_cls'] = EmailAddressAttributedValue
super().__init__(*args, **kwargs)
class PostalAddressAttributedValueField(EWSElementListField):
def __init__(self, *args, **kwargs):
from .properties import PostalAddressAttributedValue
kwargs['value_cls'] = PostalAddressAttributedValue
super().__init__(*args, **kwargs)
class GenericEventListField(EWSElementField):
"""A list field that can contain all subclasses of Event."""
is_list = True
@property
def _event_types_map(self):
return {v.response_tag(): v for v in self.value_classes}
def __init__(self, *args, **kwargs):
from .properties import CopiedEvent, CreatedEvent, DeletedEvent, ModifiedEvent, MovedEvent, \
NewMailEvent, StatusEvent, FreeBusyChangedEvent
kwargs['value_cls'] = None # Parent class requires this kwarg
kwargs['namespace'] = None # Parent class requires this kwarg
super().__init__(*args, **kwargs)
self.value_classes = (
CopiedEvent, CreatedEvent, DeletedEvent, ModifiedEvent, MovedEvent, NewMailEvent, StatusEvent,
FreeBusyChangedEvent,
)
def from_xml(self, elem, account):
events = []
for event in elem:
# This may or may not be an event element. Could also be other child elements of Notification
try:
value_cls = self._event_types_map[event.tag]
except KeyError:
continue
events.append(value_cls.from_xml(elem=event, account=account))
return events or self.default
Functions
def resolve_field_path(field_path, folder, strict=True)
-
Take the name of a field, or '__'-delimited path to a subfield, and return the corresponding Field object, label and SubField object.
Expand source code
def resolve_field_path(field_path, folder, strict=True): """Take the name of a field, or '__'-delimited path to a subfield, and return the corresponding Field object, label and SubField object. """ from .indexed_properties import SingleFieldIndexedElement, MultiFieldIndexedElement fieldname, label, subfieldname = split_field_path(field_path) field = folder.get_item_field_by_fieldname(fieldname) subfield = None if isinstance(field, IndexedField): if strict and not label: raise ValueError( "IndexedField path '%s' must specify label, e.g. '%s__%s'" % (field_path, fieldname, field.value_cls.get_field_by_fieldname('label').default) ) valid_labels = field.value_cls.get_field_by_fieldname('label').supported_choices( version=folder.account.version ) if label and label not in valid_labels: raise ValueError( "Label '%s' on IndexedField path '%s' must be one of %s" % (label, field_path, ', '.join(valid_labels)) ) if issubclass(field.value_cls, MultiFieldIndexedElement): if strict and not subfieldname: raise ValueError( "IndexedField path '%s' must specify subfield, e.g. '%s__%s__%s'" % (field_path, fieldname, label, field.value_cls.FIELDS[1].name) ) if subfieldname: try: subfield = field.value_cls.get_field_by_fieldname(subfieldname) except ValueError: fnames = ', '.join(f.name for f in field.value_cls.supported_fields( version=folder.account.version )) raise ValueError( "Subfield '%s' on IndexedField path '%s' must be one of %s" % (subfieldname, field_path, fnames) ) else: if not issubclass(field.value_cls, SingleFieldIndexedElement): raise ValueError("'field.value_cls' %r must be an SingleFieldIndexedElement instance" % field.value_cls) if subfieldname: raise ValueError( "IndexedField path '%s' must not specify subfield, e.g. just '%s__%s'" % (field_path, fieldname, label) ) subfield = field.value_cls.value_field(version=folder.account.version) else: if label or subfieldname: raise ValueError( "Field path '%s' must not specify label or subfield, e.g. just '%s'" % (field_path, fieldname) ) return field, label, subfield
def split_field_path(field_path)
-
Split a string path into its field, label and subfield parts.
:param field_path:
:return Examples: 'start' -> ('start', None, None) 'phone_numbers__PrimaryPhone' -> ('phone_numbers', 'PrimaryPhone', None) 'physical_addresses__Home__street' -> ('physical_addresses', 'Home', 'street')
Expand source code
def split_field_path(field_path): """Split a string path into its field, label and subfield parts. :param field_path: :return Examples: 'start' -> ('start', None, None) 'phone_numbers__PrimaryPhone' -> ('phone_numbers', 'PrimaryPhone', None) 'physical_addresses__Home__street' -> ('physical_addresses', 'Home', 'street') """ if not isinstance(field_path, str): raise ValueError("Field path %r must be a string" % field_path) search_parts = field_path.split('__') field = search_parts[0] try: label = search_parts[1] except IndexError: label = None try: subfield = search_parts[2] except IndexError: subfield = None return field, label, subfield
Classes
class AppointmentStateField (*args, **kwargs)
-
MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/appointmentstate
Expand source code
class AppointmentStateField(IntegerField): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/appointmentstate""" NONE = 'None' MEETING = 'Meeting' RECEIVED = 'Received' CANCELLED = 'Cancelled' STATES = { NONE: 0x0000, MEETING: 0x0001, RECEIVED: 0x0002, CANCELLED: 0x0004, } def from_xml(self, elem, account): val = super().from_xml(elem=elem, account=account) if val is None: return val return tuple(name for name, mask in self.STATES.items() if bool(val & mask))
Ancestors
Class variables
var CANCELLED
var MEETING
var NONE
var RECEIVED
var STATES
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): val = super().from_xml(elem=elem, account=account) if val is None: return val return tuple(name for name, mask in self.STATES.items() if bool(val & mask))
Inherited members
class AssociatedCalendarItemIdField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class AssociatedCalendarItemIdField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .properties import AssociatedCalendarItemId kwargs['value_cls'] = AssociatedCalendarItemId super().__init__(*args, **kwargs) def to_xml(self, value, version): return value.to_xml(version=version)
Ancestors
Class variables
var is_complex
Methods
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): return value.to_xml(version=version)
class AttachmentField (*args, **kwargs)
-
A field for item attachments.
Expand source code
class AttachmentField(EWSElementListField): """A field for item attachments.""" def __init__(self, *args, **kwargs): from .attachments import Attachment kwargs['value_cls'] = Attachment super().__init__(*args, **kwargs) def from_xml(self, elem, account): from .attachments import FileAttachment, ItemAttachment iter_elem = elem.find(self.response_tag()) # Look for both FileAttachment and ItemAttachment if iter_elem is not None: attachments = [] for att_type in (ItemAttachment, FileAttachment): attachments.extend( [att_type.from_xml(elem=e, account=account) for e in iter_elem.findall(att_type.response_tag())] ) return attachments return self.default
Ancestors
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): from .attachments import FileAttachment, ItemAttachment iter_elem = elem.find(self.response_tag()) # Look for both FileAttachment and ItemAttachment if iter_elem is not None: attachments = [] for att_type in (ItemAttachment, FileAttachment): attachments.extend( [att_type.from_xml(elem=e, account=account) for e in iter_elem.findall(att_type.response_tag())] ) return attachments return self.default
class AttendeesField (*args, **kwargs)
-
Like EWSElementField, but for lists of EWSElement objects.
Expand source code
class AttendeesField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import Attendee kwargs['value_cls'] = Attendee super().__init__(*args, **kwargs) def clean(self, value, version=None): from .properties import Mailbox if value is not None: value = [self.value_cls(mailbox=Mailbox(email_address=s), response_type='Accept') if isinstance(s, str) else s for s in value] return super().clean(value, version=version)
Ancestors
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): from .properties import Mailbox if value is not None: value = [self.value_cls(mailbox=Mailbox(email_address=s), response_type='Accept') if isinstance(s, str) else s for s in value] return super().clean(value, version=version)
class Base64Field (*args, **kwargs)
-
A field that handles binary data and automatically Base64 encodes and decodes the data.
Expand source code
class Base64Field(FieldURIField): """A field that handles binary data and automatically Base64 encodes and decodes the data.""" value_cls = bytes is_complex = True def __init__(self, *args, **kwargs): if 'is_searchable' not in kwargs: kwargs['is_searchable'] = False super().__init__(*args, **kwargs)
Ancestors
Subclasses
Class variables
var is_complex
var value_cls
-
bytes(iterable_of_ints) -> bytes bytes(string, encoding[, errors]) -> bytes bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer bytes(int) -> bytes object of size given by the parameter initialized with null bytes bytes() -> empty bytes object
Construct an immutable array of bytes from: - an iterable yielding integers in range(256) - a text string encoded using the specified encoding - any object implementing the buffer API. - an integer
class BaseEmailField (*args, **kwargs)
-
A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for.
Expand source code
class BaseEmailField(EWSElementField): """A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for.""" is_complex = True # FindItem only returns the name, not the email address def clean(self, value, version=None): if isinstance(value, str): value = self.value_cls(email_address=value) return super().clean(value, version=version) def from_xml(self, elem, account): if self.field_uri is None: sub_elem = elem.find(self.value_cls.response_tag()) else: sub_elem = elem.find(self.response_tag()) if sub_elem is not None: if self.field_uri is not None: # We want the nested Mailbox, not the wrapper element nested_elem = sub_elem.find(self.value_cls.response_tag()) if nested_elem is None: raise ValueError( 'Expected XML element %r missing on field %r' % (self.value_cls.response_tag(), self.name) ) return self.value_cls.from_xml(elem=nested_elem, account=account) return self.value_cls.from_xml(elem=sub_elem, account=account) return self.default
Ancestors
Subclasses
Class variables
var is_complex
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): if isinstance(value, str): value = self.value_cls(email_address=value) return super().clean(value, version=version)
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): if self.field_uri is None: sub_elem = elem.find(self.value_cls.response_tag()) else: sub_elem = elem.find(self.response_tag()) if sub_elem is not None: if self.field_uri is not None: # We want the nested Mailbox, not the wrapper element nested_elem = sub_elem.find(self.value_cls.response_tag()) if nested_elem is None: raise ValueError( 'Expected XML element %r missing on field %r' % (self.value_cls.response_tag(), self.name) ) return self.value_cls.from_xml(elem=nested_elem, account=account) return self.value_cls.from_xml(elem=sub_elem, account=account) return self.default
class BodyContentAttributedValueField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class BodyContentAttributedValueField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .properties import BodyContentAttributedValue kwargs['value_cls'] = BodyContentAttributedValue super().__init__(*args, **kwargs)
Ancestors
Class variables
var is_complex
class BodyField (*args, **kwargs)
-
A TextField with specific requirements for the Item body.
Expand source code
class BodyField(TextField): """A TextField with specific requirements for the Item body.""" def __init__(self, *args, **kwargs): from .properties import Body self.value_cls = Body super().__init__(*args, **kwargs) def clean(self, value, version=None): if value is not None and not isinstance(value, self.value_cls): value = self.value_cls(value) return super().clean(value, version=version) def from_xml(self, elem, account): from .properties import Body, HTMLBody field_elem = elem.find(self.response_tag()) val = None if field_elem is None else field_elem.text or None if val is not None: body_type = field_elem.get('BodyType') return { Body.body_type: Body, HTMLBody.body_type: HTMLBody, }[body_type](val) return self.default def to_xml(self, value, version): from .properties import Body, HTMLBody field_elem = create_element(self.request_tag()) body_type = { Body: Body.body_type, HTMLBody: HTMLBody.body_type, }[type(value)] field_elem.set('BodyType', body_type) return set_xml_value(field_elem, value, version=version)
Ancestors
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): if value is not None and not isinstance(value, self.value_cls): value = self.value_cls(value) return super().clean(value, version=version)
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): from .properties import Body, HTMLBody field_elem = elem.find(self.response_tag()) val = None if field_elem is None else field_elem.text or None if val is not None: body_type = field_elem.get('BodyType') return { Body.body_type: Body, HTMLBody.body_type: HTMLBody, }[body_type](val) return self.default
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): from .properties import Body, HTMLBody field_elem = create_element(self.request_tag()) body_type = { Body: Body.body_type, HTMLBody: HTMLBody.body_type, }[type(value)] field_elem.set('BodyType', body_type) return set_xml_value(field_elem, value, version=version)
Inherited members
class BooleanField (*args, **kwargs)
-
A field that handles boolean values.
Expand source code
class BooleanField(FieldURIField): """A field that handles boolean values.""" value_cls = bool
Ancestors
Subclasses
Class variables
var value_cls
-
bool(x) -> bool
Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
class BuildField (*args, **kwargs)
-
A field that stores a string value with a limited length.
Expand source code
class BuildField(CharField): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.value_cls = Build def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val: try: return self.value_cls.from_hex_string(val) except (TypeError, ValueError): log.warning('Invalid server version string: %r', val) return val
Ancestors
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val: try: return self.value_cls.from_hex_string(val) except (TypeError, ValueError): log.warning('Invalid server version string: %r', val) return val
Inherited members
class CharField (*args, **kwargs)
-
A field that stores a string value with a limited length.
Expand source code
class CharField(TextField): """A field that stores a string value with a limited length.""" is_complex = False def __init__(self, *args, **kwargs): self.max_length = kwargs.pop('max_length', 255) if not 1 <= self.max_length <= 255: # A field supporting messages longer than 255 chars should be TextField raise ValueError("'max_length' must be in the range 1-255") super().__init__(*args, **kwargs) def clean(self, value, version=None): value = super().clean(value, version=version) if value is not None: if self.is_list: for v in value: if len(v) > self.max_length: raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, v, self.max_length)) else: if len(value) > self.max_length: raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, value, self.max_length)) return value
Ancestors
Subclasses
Class variables
var is_complex
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): value = super().clean(value, version=version) if value is not None: if self.is_list: for v in value: if len(v) > self.max_length: raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, v, self.max_length)) else: if len(value) > self.max_length: raise ValueError("'%s' value '%s' exceeds length %s" % (self.name, value, self.max_length)) return value
Inherited members
class CharListField (*args, **kwargs)
-
Like CharField, but for lists of strings.
Expand source code
class CharListField(CharField): """Like CharField, but for lists of strings.""" is_list = True def __init__(self, *args, **kwargs): self.list_elem_name = kwargs.pop('list_elem_name', 'String') super().__init__(*args, **kwargs) def list_elem_tag(self): return '{%s}%s' % (self.namespace, self.list_elem_name) def from_xml(self, elem, account): iter_elem = elem.find(self.response_tag()) if iter_elem is not None: return get_xml_attrs(iter_elem, self.list_elem_tag()) return self.default
Ancestors
Subclasses
Class variables
var is_list
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): iter_elem = elem.find(self.response_tag()) if iter_elem is not None: return get_xml_attrs(iter_elem, self.list_elem_tag()) return self.default
def list_elem_tag(self)
-
Expand source code
def list_elem_tag(self): return '{%s}%s' % (self.namespace, self.list_elem_name)
Inherited members
class Choice (value, supported_from=None)
-
Implement versioned choices for the ChoiceField field.
Expand source code
class Choice: """Implement versioned choices for the ChoiceField field.""" def __init__(self, value, supported_from=None): self.value = value self.supported_from = supported_from def supports_version(self, version): # 'version' is a Version instance, for convenience by callers if not isinstance(version, Version): raise ValueError("'version' %r must be a Version instance" % version) if not self.supported_from: return True return version.build >= self.supported_from
Methods
def supports_version(self, version)
-
Expand source code
def supports_version(self, version): # 'version' is a Version instance, for convenience by callers if not isinstance(version, Version): raise ValueError("'version' %r must be a Version instance" % version) if not self.supported_from: return True return version.build >= self.supported_from
class ChoiceField (*args, **kwargs)
-
Like CharField, but restricts the value to a limited set of strings.
Expand source code
class ChoiceField(CharField): """Like CharField, but restricts the value to a limited set of strings.""" def __init__(self, *args, **kwargs): self.choices = kwargs.pop('choices') super().__init__(*args, **kwargs) def clean(self, value, version=None): value = super().clean(value, version=version) if value is None: return None valid_choices = [c.value for c in self.choices] if version: valid_choices_for_version = self.supported_choices(version=version) if value in valid_choices_for_version: return value if value in valid_choices: raise InvalidChoiceForVersion("Choice '%s' only supports EWS builds from %s to %s (server has %s)" % ( self.name, self.supported_from or '*', self.deprecated_from or '*', version)) else: if value in valid_choices: return value raise ValueError("Invalid choice '%s' for field '%s'. Valid choices are: %s" % ( value, self.name, ', '.join(valid_choices) )) def supported_choices(self, version): return [c.value for c in self.choices if c.supports_version(version)]
Ancestors
Subclasses
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): value = super().clean(value, version=version) if value is None: return None valid_choices = [c.value for c in self.choices] if version: valid_choices_for_version = self.supported_choices(version=version) if value in valid_choices_for_version: return value if value in valid_choices: raise InvalidChoiceForVersion("Choice '%s' only supports EWS builds from %s to %s (server has %s)" % ( self.name, self.supported_from or '*', self.deprecated_from or '*', version)) else: if value in valid_choices: return value raise ValueError("Invalid choice '%s' for field '%s'. Valid choices are: %s" % ( value, self.name, ', '.join(valid_choices) ))
def supported_choices(self, version)
-
Expand source code
def supported_choices(self, version): return [c.value for c in self.choices if c.supports_version(version)]
Inherited members
class CultureField (*args, **kwargs)
-
Helper to mark strings that are # RFC 1766 culture values.
Expand source code
class CultureField(CharField): """Helper to mark strings that are # RFC 1766 culture values."""
Ancestors
Inherited members
class DateField (*args, **kwargs)
-
A field that handles date values.
Expand source code
class DateField(FieldURIField): """A field that handles date values.""" value_cls = EWSDate def clean(self, value, version=None): # Allow plain datetime.date values as input if type(value) is datetime.date: value = self.value_cls.from_date(value) return super().clean(value=value, version=version)
Ancestors
Subclasses
Class variables
var value_cls
-
Extends the normal date implementation to satisfy EWS.
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): # Allow plain datetime.date values as input if type(value) is datetime.date: value = self.value_cls.from_date(value) return super().clean(value=value, version=version)
class DateOrDateTimeField (*args, **kwargs)
-
This field can handle both EWSDate and EWSDateTime. Used for calendar items where 'start' and 'end' values are conceptually dates when the calendar item is an all-day event, but datetimes in all other cases, and for recurrences where the returned 'start' and 'end' values may be either dates or datetimes depending on whether the recurring item is a task or a calendar item.
For all-day calendar items, we assume both start and end dates are inclusive.
For filtering kwarg validation and other places where we must decide on a specific class, we settle on datetime.
Expand source code
class DateOrDateTimeField(DateTimeField): """This field can handle both EWSDate and EWSDateTime. Used for calendar items where 'start' and 'end' values are conceptually dates when the calendar item is an all-day event, but datetimes in all other cases, and for recurrences where the returned 'start' and 'end' values may be either dates or datetimes depending on whether the recurring item is a task or a calendar item. For all-day calendar items, we assume both start and end dates are inclusive. For filtering kwarg validation and other places where we must decide on a specific class, we settle on datetime. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Create internal field to handle date-only logic self._date_field = DateField(*args, **kwargs) def clean(self, value, version=None): # Most calendar items will contain datetime values. We can't access the is_all_day value here, so CalendarItem # must handle that sanity check. if type(value) in (EWSDate, datetime.date): return self._date_field.clean(value=value, version=version) return super().clean(value=value, version=version) def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None and len(val) == 16: # This is a date format with timezone info, as sent by task recurrences. Eg: '2006-01-09+01:00' return self._date_field.from_xml(elem=elem, account=account) return super().from_xml(elem=elem, account=account)
Ancestors
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): # Most calendar items will contain datetime values. We can't access the is_all_day value here, so CalendarItem # must handle that sanity check. if type(value) in (EWSDate, datetime.date): return self._date_field.clean(value=value, version=version) return super().clean(value=value, version=version)
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None and len(val) == 16: # This is a date format with timezone info, as sent by task recurrences. Eg: '2006-01-09+01:00' return self._date_field.from_xml(elem=elem, account=account) return super().from_xml(elem=elem, account=account)
Inherited members
class DateTimeBackedDateField (*args, **kwargs)
-
A field that acts like a date, but where values are sent to EWS as EWSDateTime.
Expand source code
class DateTimeBackedDateField(DateField): """A field that acts like a date, but where values are sent to EWS as EWSDateTime.""" def __init__(self, *args, **kwargs): # Not all fields assume a default time of 00:00, so make this configurable self._default_time = kwargs.pop('default_time', datetime.time(0, 0)) super().__init__(*args, **kwargs) # Create internal field to handle datetime-only logic self._datetime_field = DateTimeField(*args, **kwargs) def date_to_datetime(self, value): return self._datetime_field.value_cls.combine(value, self._default_time).replace(tzinfo=UTC) def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None and len(val) == 25: # This is a datetime string with timezone info, e.g. '2021-03-01T21:55:54+00:00'. We don't want to have # datetime values converted to UTC before converting to date. EWSDateTime.from_string() insists on # converting to UTC, but we don't have an EWSTimeZone we can convert the timezone info to. Instead, parse # the string with .fromisoformat(). return datetime.datetime.fromisoformat(val).date() # Revert to default parsing of datetime strings res = self._datetime_field.from_xml(elem=elem, account=account) if res is None: return res return res.date() def to_xml(self, value, version): # Convert date to datetime value = self.date_to_datetime(value) return self._datetime_field.to_xml(value=value, version=version)
Ancestors
Methods
def date_to_datetime(self, value)
-
Expand source code
def date_to_datetime(self, value): return self._datetime_field.value_cls.combine(value, self._default_time).replace(tzinfo=UTC)
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None and len(val) == 25: # This is a datetime string with timezone info, e.g. '2021-03-01T21:55:54+00:00'. We don't want to have # datetime values converted to UTC before converting to date. EWSDateTime.from_string() insists on # converting to UTC, but we don't have an EWSTimeZone we can convert the timezone info to. Instead, parse # the string with .fromisoformat(). return datetime.datetime.fromisoformat(val).date() # Revert to default parsing of datetime strings res = self._datetime_field.from_xml(elem=elem, account=account) if res is None: return res return res.date()
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): # Convert date to datetime value = self.date_to_datetime(value) return self._datetime_field.to_xml(value=value, version=version)
Inherited members
class DateTimeField (*args, **kwargs)
-
A field that handles datetime values.
Expand source code
class DateTimeField(FieldURIField): """A field that handles datetime values.""" value_cls = EWSDateTime def clean(self, value, version=None): if isinstance(value, datetime.datetime): if not value.tzinfo: raise ValueError("Value '%s' on field '%s' must be timezone aware" % (value, self.name)) if type(value) is datetime.datetime: value = self.value_cls.from_datetime(value) return super().clean(value, version=version) def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: return self.value_cls.from_string(val) except ValueError as e: if isinstance(e, NaiveDateTimeNotAllowed): # We encountered a naive datetime if account: # Convert to timezone-aware datetime using the default timezone of the account tz = account.default_timezone log.info('Found naive datetime %s on field %s. Assuming timezone %s', e.local_dt, self.name, tz) return e.local_dt.replace(tzinfo=tz) # There's nothing we can do but return the naive date. It's better than assuming e.g. UTC. log.warning('Returning naive datetime %s on field %s', e.local_dt, self.name) return e.local_dt log.info("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls) return None return self.default
Ancestors
Subclasses
Class variables
var value_cls
-
Extends the normal datetime implementation to satisfy EWS.
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): if isinstance(value, datetime.datetime): if not value.tzinfo: raise ValueError("Value '%s' on field '%s' must be timezone aware" % (value, self.name)) if type(value) is datetime.datetime: value = self.value_cls.from_datetime(value) return super().clean(value, version=version)
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: return self.value_cls.from_string(val) except ValueError as e: if isinstance(e, NaiveDateTimeNotAllowed): # We encountered a naive datetime if account: # Convert to timezone-aware datetime using the default timezone of the account tz = account.default_timezone log.info('Found naive datetime %s on field %s. Assuming timezone %s', e.local_dt, self.name, tz) return e.local_dt.replace(tzinfo=tz) # There's nothing we can do but return the naive date. It's better than assuming e.g. UTC. log.warning('Returning naive datetime %s on field %s', e.local_dt, self.name) return e.local_dt log.info("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls) return None return self.default
class DecimalField (*args, **kwargs)
-
A field that handles decimal values.
Expand source code
class DecimalField(IntegerField): """A field that handles decimal values.""" value_cls = Decimal
Ancestors
Inherited members
class DictionaryField (*args, **kwargs)
-
A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be 'itemtype:FieldName'
Expand source code
class DictionaryField(FieldURIField): value_cls = dict def from_xml(self, elem, account): from .properties import DictionaryEntry iter_elem = elem.find(self.response_tag()) if iter_elem is not None: entries = [ DictionaryEntry.from_xml(elem=e, account=account) for e in iter_elem.findall(DictionaryEntry.response_tag()) ] return {e.key: e.value for e in entries} return self.default def clean(self, value, version=None): if isinstance(value, dict): cleaned = {} for k, v in value.items(): if type(k) is datetime.datetime: k = EWSDateTime.from_datetime(k) if type(v) is datetime.datetime: v = EWSDateTime.from_datetime(v) cleaned[k] = v value = cleaned return super().clean(value=value, version=version) def to_xml(self, value, version): from .properties import DictionaryEntry field_elem = create_element(self.request_tag()) entries = [DictionaryEntry(key=k, value=v) for k, v in value.items()] return set_xml_value(field_elem, entries, version=version)
Ancestors
Class variables
var value_cls
-
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): if isinstance(value, dict): cleaned = {} for k, v in value.items(): if type(k) is datetime.datetime: k = EWSDateTime.from_datetime(k) if type(v) is datetime.datetime: v = EWSDateTime.from_datetime(v) cleaned[k] = v value = cleaned return super().clean(value=value, version=version)
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): from .properties import DictionaryEntry iter_elem = elem.find(self.response_tag()) if iter_elem is not None: entries = [ DictionaryEntry.from_xml(elem=e, account=account) for e in iter_elem.findall(DictionaryEntry.response_tag()) ] return {e.key: e.value for e in entries} return self.default
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): from .properties import DictionaryEntry field_elem = create_element(self.request_tag()) entries = [DictionaryEntry(key=k, value=v) for k, v in value.items()] return set_xml_value(field_elem, entries, version=version)
class EWSElementField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class EWSElementField(FieldURIField): """A generic field for any EWSElement object.""" def __init__(self, *args, **kwargs): self._value_cls = kwargs.pop('value_cls') if 'namespace' not in kwargs: kwargs['namespace'] = self.value_cls.NAMESPACE super().__init__(*args, **kwargs) @property def value_cls(self): if isinstance(self._value_cls, str): # Support 'value_cls' as string to allow self-referencing classes. The class must be importable from the # top-level module. self._value_cls = getattr(import_module(self.__module__.split('.')[0]), self._value_cls) return self._value_cls def from_xml(self, elem, account): if self.is_list: iter_elem = elem.find(self.response_tag()) if iter_elem is not None: return [self.value_cls.from_xml(elem=e, account=account) for e in iter_elem.findall(self.value_cls.response_tag())] else: if self.field_uri is None: sub_elem = elem.find(self.value_cls.response_tag()) else: sub_elem = elem.find(self.response_tag()) if sub_elem is not None: return self.value_cls.from_xml(elem=sub_elem, account=account) return self.default def to_xml(self, value, version): if self.field_uri is None: return value.to_xml(version=version) field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version)
Ancestors
Subclasses
- AssociatedCalendarItemIdField
- BaseEmailField
- BodyContentAttributedValueField
- EWSElementListField
- EffectiveRightsField
- GenericEventListField
- IdElementField
- IndexedField
- OccurrenceField
- PermissionSetField
- PersonaPhoneNumberField
- RecurrenceField
- ReferenceItemIdField
- TaskRecurrenceField
Instance variables
var value_cls
-
Expand source code
@property def value_cls(self): if isinstance(self._value_cls, str): # Support 'value_cls' as string to allow self-referencing classes. The class must be importable from the # top-level module. self._value_cls = getattr(import_module(self.__module__.split('.')[0]), self._value_cls) return self._value_cls
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): if self.is_list: iter_elem = elem.find(self.response_tag()) if iter_elem is not None: return [self.value_cls.from_xml(elem=e, account=account) for e in iter_elem.findall(self.value_cls.response_tag())] else: if self.field_uri is None: sub_elem = elem.find(self.value_cls.response_tag()) else: sub_elem = elem.find(self.response_tag()) if sub_elem is not None: return self.value_cls.from_xml(elem=sub_elem, account=account) return self.default
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): if self.field_uri is None: return value.to_xml(version=version) field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version)
class EWSElementListField (*args, **kwargs)
-
Like EWSElementField, but for lists of EWSElement objects.
Expand source code
class EWSElementListField(EWSElementField): """Like EWSElementField, but for lists of EWSElement objects.""" is_list = True is_complex = True
Ancestors
Subclasses
- AttachmentField
- AttendeesField
- EmailAddressAttributedValueField
- MailboxListField
- MemberListField
- MessageHeaderField
- PhoneNumberAttributedValueField
- PostalAddressAttributedValueField
- ProtocolListField
- StringAttributedValueField
Class variables
var is_complex
var is_list
class EffectiveRightsField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class EffectiveRightsField(EWSElementField): def __init__(self, *args, **kwargs): from .properties import EffectiveRights kwargs['value_cls'] = EffectiveRights super().__init__(*args, **kwargs)
Ancestors
class EmailAddressAttributedValueField (*args, **kwargs)
-
Like EWSElementField, but for lists of EWSElement objects.
Expand source code
class EmailAddressAttributedValueField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import EmailAddressAttributedValue kwargs['value_cls'] = EmailAddressAttributedValue super().__init__(*args, **kwargs)
Ancestors
class EmailAddressField (*args, **kwargs)
-
A helper class used for email address string that we can use for email validation.
Expand source code
class EmailAddressField(CharField): """A helper class used for email address string that we can use for email validation."""
Ancestors
Inherited members
class EmailAddressesField (*args, **kwargs)
-
A base class for all indexed fields.
Expand source code
class EmailAddressesField(IndexedField): is_list = True PARENT_ELEMENT_NAME = 'EmailAddresses' def __init__(self, *args, **kwargs): from .indexed_properties import EmailAddress kwargs['value_cls'] = EmailAddress super().__init__(*args, **kwargs) def clean(self, value, version=None): if value is not None: default_labels = self.value_cls.LABEL_CHOICES if len(value) > len(default_labels): raise ValueError('This field can handle at most %s values (value: %r)' % (len(default_labels), value)) tmp = [] for s, default_label in zip(value, default_labels): if not isinstance(s, str): tmp.append(s) continue tmp.append(self.value_cls(email=s, label=default_label)) value = tmp return super().clean(value, version=version)
Ancestors
Class variables
var PARENT_ELEMENT_NAME
var is_list
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): if value is not None: default_labels = self.value_cls.LABEL_CHOICES if len(value) > len(default_labels): raise ValueError('This field can handle at most %s values (value: %r)' % (len(default_labels), value)) tmp = [] for s, default_label in zip(value, default_labels): if not isinstance(s, str): tmp.append(s) continue tmp.append(self.value_cls(email=s, label=default_label)) value = tmp return super().clean(value, version=version)
class EmailField (*args, **kwargs)
-
A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for.
Expand source code
class EmailField(BaseEmailField): def __init__(self, *args, **kwargs): from .properties import Email kwargs['value_cls'] = Email super().__init__(*args, **kwargs)
Ancestors
class EmailSubField (name=None, is_required=False, is_required_after_save=False, is_read_only=False, is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None, supported_from=None, deprecated_from=None)
-
A field to hold the value on an SingleFieldIndexedElement.
Expand source code
class EmailSubField(SubField): """A field to hold the value on an SingleFieldIndexedElement.""" value_cls = str def from_xml(self, elem, account): return elem.text or elem.get('Name') # Sometimes elem.text is empty. Exchange saves the same in 'Name' attr
Ancestors
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): return elem.text or elem.get('Name') # Sometimes elem.text is empty. Exchange saves the same in 'Name' attr
Inherited members
class EnumAsIntField (*args, **kwargs)
-
Like EnumField, but communicates values with EWS in integers.
Expand source code
class EnumAsIntField(EnumField): """Like EnumField, but communicates values with EWS in integers.""" def from_xml(self, elem, account): return super(EnumField, self).from_xml(elem=elem, account=account) def to_xml(self, value, version): field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version)
Ancestors
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): return super(EnumField, self).from_xml(elem=elem, account=account)
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version)
Inherited members
class EnumField (*args, **kwargs)
-
A field type where you can enter either the 1-based index in an enum (tuple), or the enum value. Values will be stored internally as integers but output in XML as strings.
Expand source code
class EnumField(IntegerField): """A field type where you can enter either the 1-based index in an enum (tuple), or the enum value. Values will be stored internally as integers but output in XML as strings. """ def __init__(self, *args, **kwargs): self.enum = kwargs.pop('enum') # Set different min/max defaults than IntegerField if 'max' in kwargs: raise AttributeError("EnumField does not support the 'max' attribute") kwargs['min'] = kwargs.pop('min', 1) kwargs['max'] = kwargs['min'] + len(self.enum) - 1 super().__init__(*args, **kwargs) def clean(self, value, version=None): if self.is_list: value = list(value) # Convert to something we can index for i, v in enumerate(value): if isinstance(v, str): if v not in self.enum: raise ValueError( "List value '%s' on field '%s' must be one of %s" % (v, self.name, self.enum)) value[i] = self.enum.index(v) + 1 if not value: raise ValueError("Value '%s' on field '%s' must not be empty" % (value, self.name)) if len(value) > len(set(value)): raise ValueError("List entries '%s' on field '%s' must be unique" % (value, self.name)) else: if isinstance(value, str): if value not in self.enum: raise ValueError( "Value '%s' on field '%s' must be one of %s" % (value, self.name, self.enum)) value = self.enum.index(value) + 1 return super().clean(value, version=version) def as_string(self, value): # Converts an integer in the enum to its equivalent string if isinstance(value, str): return value if self.is_list: return [self.enum[v - 1] for v in sorted(value)] return self.enum[value - 1] def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: if self.is_list: return [self.enum.index(v) + 1 for v in val.split(' ')] return self.enum.index(val) + 1 except ValueError: log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls) return None return self.default def to_xml(self, value, version): field_elem = create_element(self.request_tag()) if self.is_list: return set_xml_value(field_elem, ' '.join(self.as_string(value)), version=version) return set_xml_value(field_elem, self.as_string(value), version=version)
Ancestors
Subclasses
Methods
def as_string(self, value)
-
Expand source code
def as_string(self, value): # Converts an integer in the enum to its equivalent string if isinstance(value, str): return value if self.is_list: return [self.enum[v - 1] for v in sorted(value)] return self.enum[value - 1]
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): if self.is_list: value = list(value) # Convert to something we can index for i, v in enumerate(value): if isinstance(v, str): if v not in self.enum: raise ValueError( "List value '%s' on field '%s' must be one of %s" % (v, self.name, self.enum)) value[i] = self.enum.index(v) + 1 if not value: raise ValueError("Value '%s' on field '%s' must not be empty" % (value, self.name)) if len(value) > len(set(value)): raise ValueError("List entries '%s' on field '%s' must be unique" % (value, self.name)) else: if isinstance(value, str): if value not in self.enum: raise ValueError( "Value '%s' on field '%s' must be one of %s" % (value, self.name, self.enum)) value = self.enum.index(value) + 1 return super().clean(value, version=version)
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: if self.is_list: return [self.enum.index(v) + 1 for v in val.split(' ')] return self.enum.index(val) + 1 except ValueError: log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls) return None return self.default
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): field_elem = create_element(self.request_tag()) if self.is_list: return set_xml_value(field_elem, ' '.join(self.as_string(value)), version=version) return set_xml_value(field_elem, self.as_string(value), version=version)
Inherited members
class EnumListField (*args, **kwargs)
-
Like EnumField, but for lists of enum values.
Expand source code
class EnumListField(EnumField): """Like EnumField, but for lists of enum values.""" is_list = True
Ancestors
Class variables
var is_list
Inherited members
class ExtendedPropertyField (*args, **kwargs)
-
Holds information related to an item field.
Expand source code
class ExtendedPropertyField(Field): def __init__(self, *args, **kwargs): self.value_cls = kwargs.pop('value_cls') super().__init__(*args, **kwargs) def clean(self, value, version=None): if value is None: if self.is_required: raise ValueError("'%s' is a required field" % self.name) return self.default if not isinstance(value, self.value_cls): # Allow keeping ExtendedProperty field values as their simple Python type, but run clean() anyway tmp = self.value_cls(value) tmp.clean(version=version) return value value.clean(version=version) return value def field_uri_xml(self): from .properties import ExtendedFieldURI cls = self.value_cls 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, ).to_xml(version=None) def from_xml(self, elem, account): extended_properties = elem.findall(self.value_cls.response_tag()) for extended_property in extended_properties: if self.value_cls.is_property_instance(extended_property): return self.value_cls.from_xml(elem=extended_property, account=account) return self.default def to_xml(self, value, version): extended_property = create_element(self.value_cls.request_tag()) set_xml_value(extended_property, self.field_uri_xml(), version=version) if isinstance(value, self.value_cls): set_xml_value(extended_property, value, version=version) else: # Allow keeping ExtendedProperty field values as their simple Python type set_xml_value(extended_property, self.value_cls(value), version=version) return extended_property def __hash__(self): return hash(self.name)
Ancestors
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): if value is None: if self.is_required: raise ValueError("'%s' is a required field" % self.name) return self.default if not isinstance(value, self.value_cls): # Allow keeping ExtendedProperty field values as their simple Python type, but run clean() anyway tmp = self.value_cls(value) tmp.clean(version=version) return value value.clean(version=version) return value
def field_uri_xml(self)
-
Expand source code
def field_uri_xml(self): from .properties import ExtendedFieldURI cls = self.value_cls 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, ).to_xml(version=None)
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): extended_properties = elem.findall(self.value_cls.response_tag()) for extended_property in extended_properties: if self.value_cls.is_property_instance(extended_property): return self.value_cls.from_xml(elem=extended_property, account=account) return self.default
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): extended_property = create_element(self.value_cls.request_tag()) set_xml_value(extended_property, self.field_uri_xml(), version=version) if isinstance(value, self.value_cls): set_xml_value(extended_property, value, version=version) else: # Allow keeping ExtendedProperty field values as their simple Python type set_xml_value(extended_property, self.value_cls(value), version=version) return extended_property
class Field (name=None, is_required=False, is_required_after_save=False, is_read_only=False, is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None, supported_from=None, deprecated_from=None)
-
Holds information related to an item field.
Expand source code
class Field(metaclass=abc.ABCMeta): """Holds information related to an item field.""" value_cls = None is_list = False # Is the field a complex EWS type? Quoting the EWS FindItem docs: # # The FindItem operation returns only the first 512 bytes of any streamable property. For Unicode, it returns # the first 255 characters by using a null-terminated Unicode string. It does not return any of the message # body formats or the recipient lists. # is_complex = False def __init__(self, name=None, is_required=False, is_required_after_save=False, is_read_only=False, is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None, supported_from=None, deprecated_from=None): self.name = name self.default = default # Default value if none is given self.is_required = is_required # Some fields cannot be deleted on update. Default to True if 'is_required' is set self.is_required_after_save = is_required or is_required_after_save self.is_read_only = is_read_only # Set this for fields that raise ErrorInvalidPropertyUpdateSentMessage on update after send. Default to True # if 'is_read_only' is set self.is_read_only_after_send = is_read_only or is_read_only_after_send # Define whether the field can be used in a QuerySet. For some reason, EWS disallows searching on some fields, # instead throwing ErrorInvalidValueForProperty self.is_searchable = is_searchable # When true, this field is treated as an XML attribute instead of an element self.is_attribute = is_attribute # The Exchange build when this field was introduced. When talking with versions prior to this version, # we will ignore this field. if supported_from is not None and not isinstance(supported_from, Build): raise ValueError("'supported_from' %r must be a Build instance" % supported_from) self.supported_from = supported_from # The Exchange build when this field was deprecated. When talking with versions at or later than this version, # we will ignore this field. if deprecated_from is not None and not isinstance(deprecated_from, Build): raise ValueError("'deprecated_from' %r must be a Build instance" % deprecated_from) self.deprecated_from = deprecated_from def clean(self, value, version=None): if version and not self.supports_version(version): raise InvalidFieldForVersion("Field '%s' does not support EWS builds prior to %s (server has %s)" % ( self.name, self.supported_from, version)) if value is None: if self.is_required and self.default is None: raise ValueError("'%s' is a required field with no default" % self.name) return self.default if self.is_list: if not is_iterable(value): raise ValueError("Field '%s' value %r must be a list" % (self.name, value)) for v in value: if not isinstance(v, self.value_cls): raise TypeError("Field '%s' value %r must be of type %s" % (self.name, v, self.value_cls)) if hasattr(v, 'clean'): v.clean(version=version) else: if not isinstance(value, self.value_cls): raise TypeError("Field '%s' value %r must be of type %s" % (self.name, value, self.value_cls)) if hasattr(value, 'clean'): value.clean(version=version) return value @abc.abstractmethod def from_xml(self, elem, account): pass @abc.abstractmethod def to_xml(self, value, version): pass def supports_version(self, version): # 'version' is a Version instance, for convenience by callers if not isinstance(version, Version): raise ValueError("'version' %r must be a Version instance" % version) if self.supported_from and version.build < self.supported_from: return False if self.deprecated_from and version.build >= self.deprecated_from: return False return True def __eq__(self, other): return hash(self) == hash(other) @abc.abstractmethod def __hash__(self): pass def __repr__(self): return self.__class__.__name__ + '(%s)' % ', '.join('%s=%r' % (f, getattr(self, f)) for f in ( 'name', 'value_cls', 'is_list', 'is_complex', 'default'))
Subclasses
Class variables
var is_complex
var is_list
var value_cls
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): if version and not self.supports_version(version): raise InvalidFieldForVersion("Field '%s' does not support EWS builds prior to %s (server has %s)" % ( self.name, self.supported_from, version)) if value is None: if self.is_required and self.default is None: raise ValueError("'%s' is a required field with no default" % self.name) return self.default if self.is_list: if not is_iterable(value): raise ValueError("Field '%s' value %r must be a list" % (self.name, value)) for v in value: if not isinstance(v, self.value_cls): raise TypeError("Field '%s' value %r must be of type %s" % (self.name, v, self.value_cls)) if hasattr(v, 'clean'): v.clean(version=version) else: if not isinstance(value, self.value_cls): raise TypeError("Field '%s' value %r must be of type %s" % (self.name, value, self.value_cls)) if hasattr(value, 'clean'): value.clean(version=version) return value
def from_xml(self, elem, account)
-
Expand source code
@abc.abstractmethod def from_xml(self, elem, account): pass
def supports_version(self, version)
-
Expand source code
def supports_version(self, version): # 'version' is a Version instance, for convenience by callers if not isinstance(version, Version): raise ValueError("'version' %r must be a Version instance" % version) if self.supported_from and version.build < self.supported_from: return False if self.deprecated_from and version.build >= self.deprecated_from: return False return True
def to_xml(self, value, version)
-
Expand source code
@abc.abstractmethod def to_xml(self, value, version): pass
class FieldOrder (field_path, reverse=False)
-
Holds values needed to call server-side sorting on a single field path.
Expand source code
class FieldOrder: """Holds values needed to call server-side sorting on a single field path.""" def __init__(self, field_path, reverse=False): if not isinstance(field_path, FieldPath): raise ValueError("'field_path' %r must be a FieldPath instance" % field_path) if not isinstance(reverse, bool): raise ValueError("'reverse' %r must be a boolean" % reverse) self.field_path = field_path self.reverse = reverse @classmethod def from_string(cls, field_path, folder): return cls( field_path=FieldPath.from_string(field_path=field_path.lstrip('-'), folder=folder, strict=True), reverse=field_path.startswith('-') ) def to_xml(self): field_order = create_element('t:FieldOrder', attrs=dict(Order='Descending' if self.reverse else 'Ascending')) field_order.append(self.field_path.to_xml()) return field_order
Static methods
def from_string(field_path, folder)
-
Expand source code
@classmethod def from_string(cls, field_path, folder): return cls( field_path=FieldPath.from_string(field_path=field_path.lstrip('-'), folder=folder, strict=True), reverse=field_path.startswith('-') )
Methods
def to_xml(self)
-
Expand source code
def to_xml(self): field_order = create_element('t:FieldOrder', attrs=dict(Order='Descending' if self.reverse else 'Ascending')) field_order.append(self.field_path.to_xml()) return field_order
class FieldPath (field, label=None, subfield=None)
-
Holds values needed to point to a single field. For indexed properties, we allow setting either field, field and label, or field, label and subfield. This allows pointing to either the full indexed property set, a property with a specific label, or a particular subfield field on that property.
Expand source code
class FieldPath: """Holds values needed to point to a single field. For indexed properties, we allow setting either field, field and label, or field, label and subfield. This allows pointing to either the full indexed property set, a property with a specific label, or a particular subfield field on that property. """ def __init__(self, field, label=None, subfield=None): # 'label' and 'subfield' are only used for IndexedField fields if not isinstance(field, (FieldURIField, ExtendedPropertyField)): raise ValueError("'field' %r must be an FieldURIField, of ExtendedPropertyField instance" % field) if label and not isinstance(label, str): raise ValueError("'label' %r must be a %s instance" % (label, str)) if subfield and not isinstance(subfield, SubField): raise ValueError("'subfield' %r must be a SubField instance" % subfield) self.field = field self.label = label self.subfield = subfield @classmethod def from_string(cls, field_path, folder, strict=False): field, label, subfield = resolve_field_path(field_path, folder=folder, strict=strict) return cls(field=field, label=label, subfield=subfield) def get_value(self, item): # For indexed properties, get either the full property set, the property with matching label, or a particular # subfield. if self.label: for subitem in getattr(item, self.field.name): if subitem.label == self.label: if self.subfield: return getattr(subitem, self.subfield.name) return subitem return None # No item with this label return getattr(item, self.field.name) def get_sort_value(self, item): # For fields that allow values of different types, we need to return a value that is val = self.get_value(item) if isinstance(self.field, DateOrDateTimeField) and isinstance(val, EWSDate): return item.date_to_datetime(field_name=self.field.name) return val def to_xml(self): if isinstance(self.field, IndexedField): if not self.label or not self.subfield: raise ValueError("Field path for indexed field '%s' is missing label and/or subfield" % self.field.name) return self.subfield.field_uri_xml(field_uri=self.field.field_uri, label=self.label) return self.field.field_uri_xml() def expand(self, version): # If this path does not point to a specific subfield on an indexed property, return all the possible path # combinations for this field path. if isinstance(self.field, IndexedField): labels = [self.label] if self.label \ else self.field.value_cls.get_field_by_fieldname('label').supported_choices(version=version) subfields = [self.subfield] if self.subfield else self.field.value_cls.supported_fields(version=version) for label in labels: for subfield in subfields: yield FieldPath(field=self.field, label=label, subfield=subfield) else: yield self @property def path(self): if self.label: from .indexed_properties import SingleFieldIndexedElement if issubclass(self.field.value_cls, SingleFieldIndexedElement) or not self.subfield: return '%s__%s' % (self.field.name, self.label) return '%s__%s__%s' % (self.field.name, self.label, self.subfield.name) return self.field.name def __eq__(self, other): return hash(self) == hash(other) def __str__(self): return self.path def __repr__(self): return self.__class__.__name__ + repr((self.field, self.label, self.subfield)) def __hash__(self): return hash((self.field, self.label, self.subfield))
Static methods
def from_string(field_path, folder, strict=False)
-
Expand source code
@classmethod def from_string(cls, field_path, folder, strict=False): field, label, subfield = resolve_field_path(field_path, folder=folder, strict=strict) return cls(field=field, label=label, subfield=subfield)
Instance variables
var path
-
Expand source code
@property def path(self): if self.label: from .indexed_properties import SingleFieldIndexedElement if issubclass(self.field.value_cls, SingleFieldIndexedElement) or not self.subfield: return '%s__%s' % (self.field.name, self.label) return '%s__%s__%s' % (self.field.name, self.label, self.subfield.name) return self.field.name
Methods
def expand(self, version)
-
Expand source code
def expand(self, version): # If this path does not point to a specific subfield on an indexed property, return all the possible path # combinations for this field path. if isinstance(self.field, IndexedField): labels = [self.label] if self.label \ else self.field.value_cls.get_field_by_fieldname('label').supported_choices(version=version) subfields = [self.subfield] if self.subfield else self.field.value_cls.supported_fields(version=version) for label in labels: for subfield in subfields: yield FieldPath(field=self.field, label=label, subfield=subfield) else: yield self
def get_sort_value(self, item)
-
Expand source code
def get_sort_value(self, item): # For fields that allow values of different types, we need to return a value that is val = self.get_value(item) if isinstance(self.field, DateOrDateTimeField) and isinstance(val, EWSDate): return item.date_to_datetime(field_name=self.field.name) return val
def get_value(self, item)
-
Expand source code
def get_value(self, item): # For indexed properties, get either the full property set, the property with matching label, or a particular # subfield. if self.label: for subitem in getattr(item, self.field.name): if subitem.label == self.label: if self.subfield: return getattr(subitem, self.subfield.name) return subitem return None # No item with this label return getattr(item, self.field.name)
def to_xml(self)
-
Expand source code
def to_xml(self): if isinstance(self.field, IndexedField): if not self.label or not self.subfield: raise ValueError("Field path for indexed field '%s' is missing label and/or subfield" % self.field.name) return self.subfield.field_uri_xml(field_uri=self.field.field_uri, label=self.label) return self.field.field_uri_xml()
class FieldURIField (*args, **kwargs)
-
A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be 'itemtype:FieldName'
Expand source code
class FieldURIField(Field): """A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be 'itemtype:FieldName' """ def __init__(self, *args, **kwargs): self.field_uri = kwargs.pop('field_uri', None) self.namespace = kwargs.pop('namespace', TNS) super().__init__(*args, **kwargs) # See all valid FieldURI values at # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/fielduri # The field_uri has a prefix when the FieldURI points to an Item field. if self.field_uri is None: self.field_uri_postfix = None elif ':' in self.field_uri: self.field_uri_postfix = self.field_uri.split(':')[1] else: self.field_uri_postfix = self.field_uri def _get_val_from_elem(self, elem): if self.is_attribute: return elem.get(self.field_uri) or None return get_xml_attr(elem, self.response_tag()) def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: return xml_text_to_value(val, self.value_cls) except (ValueError, InvalidOperation): log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls) return None return self.default def to_xml(self, value, version): field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version) def field_uri_xml(self): from .properties import FieldURI if not self.field_uri: raise ValueError("'field_uri' value is missing") return FieldURI(field_uri=self.field_uri).to_xml(version=None) def request_tag(self): if not self.field_uri_postfix: raise ValueError("'field_uri_postfix' value is missing") return 't:%s' % self.field_uri_postfix def response_tag(self): if not self.field_uri_postfix: raise ValueError("'field_uri_postfix' value is missing") return '{%s}%s' % (self.namespace, self.field_uri_postfix) def __hash__(self): return hash(self.field_uri)
Ancestors
Subclasses
- Base64Field
- BooleanField
- DateField
- DateTimeField
- DictionaryField
- EWSElementField
- IntegerField
- ItemField
- TextField
- TimeField
- TimeZoneField
- TypeValueField
Methods
def field_uri_xml(self)
-
Expand source code
def field_uri_xml(self): from .properties import FieldURI if not self.field_uri: raise ValueError("'field_uri' value is missing") return FieldURI(field_uri=self.field_uri).to_xml(version=None)
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: return xml_text_to_value(val, self.value_cls) except (ValueError, InvalidOperation): log.warning("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls) return None return self.default
def request_tag(self)
-
Expand source code
def request_tag(self): if not self.field_uri_postfix: raise ValueError("'field_uri_postfix' value is missing") return 't:%s' % self.field_uri_postfix
def response_tag(self)
-
Expand source code
def response_tag(self): if not self.field_uri_postfix: raise ValueError("'field_uri_postfix' value is missing") return '{%s}%s' % (self.namespace, self.field_uri_postfix)
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version)
class FreeBusyStatusField (*args, **kwargs)
-
Like ChoiceField, but specifically for Free/Busy values.
Expand source code
class FreeBusyStatusField(ChoiceField): """Like ChoiceField, but specifically for Free/Busy values.""" def __init__(self, *args, **kwargs): kwargs['choices'] = set(FREE_BUSY_CHOICES) super().__init__(*args, **kwargs)
Ancestors
Inherited members
class GenericEventListField (*args, **kwargs)
-
A list field that can contain all subclasses of Event.
Expand source code
class GenericEventListField(EWSElementField): """A list field that can contain all subclasses of Event.""" is_list = True @property def _event_types_map(self): return {v.response_tag(): v for v in self.value_classes} def __init__(self, *args, **kwargs): from .properties import CopiedEvent, CreatedEvent, DeletedEvent, ModifiedEvent, MovedEvent, \ NewMailEvent, StatusEvent, FreeBusyChangedEvent kwargs['value_cls'] = None # Parent class requires this kwarg kwargs['namespace'] = None # Parent class requires this kwarg super().__init__(*args, **kwargs) self.value_classes = ( CopiedEvent, CreatedEvent, DeletedEvent, ModifiedEvent, MovedEvent, NewMailEvent, StatusEvent, FreeBusyChangedEvent, ) def from_xml(self, elem, account): events = [] for event in elem: # This may or may not be an event element. Could also be other child elements of Notification try: value_cls = self._event_types_map[event.tag] except KeyError: continue events.append(value_cls.from_xml(elem=event, account=account)) return events or self.default
Ancestors
Class variables
var is_list
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): events = [] for event in elem: # This may or may not be an event element. Could also be other child elements of Notification try: value_cls = self._event_types_map[event.tag] except KeyError: continue events.append(value_cls.from_xml(elem=event, account=account)) return events or self.default
class IdElementField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class IdElementField(EWSElementField): def __init__(self, *args, **kwargs): kwargs['is_searchable'] = False kwargs['is_read_only'] = True super().__init__(*args, **kwargs)
Ancestors
class IdField (*args, **kwargs)
-
A field to hold the 'Id' and 'Changekey' attributes on 'ItemId' type items. There is no guaranteed max length, but we can assume 512 bytes in practice. See https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/ews-identifiers-in-exchange
Expand source code
class IdField(CharField): """A field to hold the 'Id' and 'Changekey' attributes on 'ItemId' type items. There is no guaranteed max length, but we can assume 512 bytes in practice. See https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/ews-identifiers-in-exchange """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.max_length = 512 # This is above the normal 255 limit, but this is actually an attribute, not a field self.is_searchable = False self.is_attribute = True
Ancestors
Inherited members
class IndexedField (*args, **kwargs)
-
A base class for all indexed fields.
Expand source code
class IndexedField(EWSElementField): """A base class for all indexed fields.""" PARENT_ELEMENT_NAME = None def __init__(self, *args, **kwargs): from .indexed_properties import IndexedElement value_cls = kwargs['value_cls'] if not issubclass(value_cls, IndexedElement): raise ValueError("'value_cls' %r must be a subclass of IndexedElement" % value_cls) super().__init__(*args, **kwargs) def to_xml(self, value, version): return set_xml_value(create_element('t:%s' % self.PARENT_ELEMENT_NAME), value, version) def response_tag(self): return '{%s}%s' % (self.namespace, self.PARENT_ELEMENT_NAME) def __hash__(self): return hash(self.field_uri)
Ancestors
Subclasses
Class variables
var PARENT_ELEMENT_NAME
Methods
def response_tag(self)
-
Expand source code
def response_tag(self): return '{%s}%s' % (self.namespace, self.PARENT_ELEMENT_NAME)
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): return set_xml_value(create_element('t:%s' % self.PARENT_ELEMENT_NAME), value, version)
class IntegerField (*args, **kwargs)
-
A field that handles integer values.
Expand source code
class IntegerField(FieldURIField): """A field that handles integer values.""" value_cls = int def __init__(self, *args, **kwargs): self.min = kwargs.pop('min', None) self.max = kwargs.pop('max', None) super().__init__(*args, **kwargs) def _clean_single_value(self, v): if self.min is not None and v < self.min: raise ValueError( "Value %r on field '%s' must be greater than %s" % (v, self.name, self.min)) if self.max is not None and v > self.max: raise ValueError("Value %r on field '%s' must be less than %s" % (v, self.name, self.max)) def clean(self, value, version=None): value = super().clean(value, version=version) if value is not None: if self.is_list: for v in value: self._clean_single_value(v) else: self._clean_single_value(value) return value
Ancestors
Subclasses
Class variables
var value_cls
-
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0) 4
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): value = super().clean(value, version=version) if value is not None: if self.is_list: for v in value: self._clean_single_value(v) else: self._clean_single_value(value) return value
class InvalidChoiceForVersion (*args, **kwargs)
-
Used when a value is not valid for an enum-type field.
Expand source code
class InvalidChoiceForVersion(ValueError): """Used when a value is not valid for an enum-type field."""
Ancestors
- builtins.ValueError
- builtins.Exception
- builtins.BaseException
class InvalidField (*args, **kwargs)
-
Used when a field name does not match any defined fields.
Expand source code
class InvalidField(ValueError): """Used when a field name does not match any defined fields."""
Ancestors
- builtins.ValueError
- builtins.Exception
- builtins.BaseException
class InvalidFieldForVersion (*args, **kwargs)
-
Used when a field is not supported on the given Exchnage version.
Expand source code
class InvalidFieldForVersion(ValueError): """Used when a field is not supported on the given Exchnage version."""
Ancestors
- builtins.ValueError
- builtins.Exception
- builtins.BaseException
class ItemField (*args, **kwargs)
-
A field that has a FieldURI value in EWS. This means it's value is contained in an XML element or arrtibute. It may additionally be a label for searching, filtering and limiting fields. In that case, the FieldURI format will be 'itemtype:FieldName'
Expand source code
class ItemField(FieldURIField): @property def value_cls(self): from .items import Item return Item def from_xml(self, elem, account): from .items import ITEM_CLASSES for item_cls in ITEM_CLASSES: item_elem = elem.find(item_cls.response_tag()) if item_elem is not None: return item_cls.from_xml(elem=item_elem, account=account) return None def to_xml(self, value, version): # We don't want to wrap in an Item element return value.to_xml(version=version)
Ancestors
Instance variables
var value_cls
-
Expand source code
@property def value_cls(self): from .items import Item return Item
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): from .items import ITEM_CLASSES for item_cls in ITEM_CLASSES: item_elem = elem.find(item_cls.response_tag()) if item_elem is not None: return item_cls.from_xml(elem=item_elem, account=account) return None
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): # We don't want to wrap in an Item element return value.to_xml(version=version)
class LabelField (*args, **kwargs)
-
A field to hold the label on an IndexedElement.
Expand source code
class LabelField(ChoiceField): """A field to hold the label on an IndexedElement.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.is_attribute = True def from_xml(self, elem, account): return elem.get(self.field_uri)
Ancestors
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): return elem.get(self.field_uri)
Inherited members
class MailboxField (*args, **kwargs)
-
A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for.
Expand source code
class MailboxField(BaseEmailField): def __init__(self, *args, **kwargs): from .properties import Mailbox kwargs['value_cls'] = Mailbox super().__init__(*args, **kwargs)
Ancestors
class MailboxListField (*args, **kwargs)
-
Like EWSElementField, but for lists of EWSElement objects.
Expand source code
class MailboxListField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import Mailbox kwargs['value_cls'] = Mailbox super().__init__(*args, **kwargs) def clean(self, value, version=None): if value is not None: value = [self.value_cls(email_address=s) if isinstance(s, str) else s for s in value] return super().clean(value, version=version)
Ancestors
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): if value is not None: value = [self.value_cls(email_address=s) if isinstance(s, str) else s for s in value] return super().clean(value, version=version)
class MemberListField (*args, **kwargs)
-
Like EWSElementField, but for lists of EWSElement objects.
Expand source code
class MemberListField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import Member kwargs['value_cls'] = Member super().__init__(*args, **kwargs) def clean(self, value, version=None): from .properties import Mailbox if value is not None: value = [ self.value_cls(mailbox=Mailbox(email_address=s)) if isinstance(s, str) else s for s in value ] return super().clean(value, version=version)
Ancestors
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): from .properties import Mailbox if value is not None: value = [ self.value_cls(mailbox=Mailbox(email_address=s)) if isinstance(s, str) else s for s in value ] return super().clean(value, version=version)
class MessageField (*args, **kwargs)
-
A field that handles the Message element.
Expand source code
class MessageField(TextField): """A field that handles the Message element.""" INNER_ELEMENT_NAME = 'Message' def from_xml(self, elem, account): reply = elem.find(self.response_tag()) if reply is None: return None message = reply.find('{%s}%s' % (TNS, self.INNER_ELEMENT_NAME)) if message is None: return None return message.text def to_xml(self, value, version): field_elem = create_element(self.request_tag()) message = create_element('t:%s' % self.INNER_ELEMENT_NAME) message.text = value return set_xml_value(field_elem, message, version=version)
Ancestors
Class variables
var INNER_ELEMENT_NAME
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): reply = elem.find(self.response_tag()) if reply is None: return None message = reply.find('{%s}%s' % (TNS, self.INNER_ELEMENT_NAME)) if message is None: return None return message.text
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): field_elem = create_element(self.request_tag()) message = create_element('t:%s' % self.INNER_ELEMENT_NAME) message.text = value return set_xml_value(field_elem, message, version=version)
Inherited members
class MessageHeaderField (*args, **kwargs)
-
Like EWSElementField, but for lists of EWSElement objects.
Expand source code
class MessageHeaderField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import MessageHeader kwargs['value_cls'] = MessageHeader super().__init__(*args, **kwargs)
Ancestors
class MimeContentField (*args, **kwargs)
-
Like Base64Field. This element has an optional 'CharacterSet' attribute, but it specifies the encoding of the base64-encoded string (which doesn't make sense since base64-encoded strings are always ASCII). We ignore it here because the decoded data could be in some other encoding, specified in the "Content-Type" HTTP header.
Expand source code
class MimeContentField(Base64Field): """Like Base64Field. This element has an optional 'CharacterSet' attribute, but it specifies the encoding of the base64-encoded string (which doesn't make sense since base64-encoded strings are always ASCII). We ignore it here because the decoded data could be in some other encoding, specified in the "Content-Type" HTTP header. """
Ancestors
Inherited members
class NamedSubField (*args, **kwargs)
-
A field to hold the value on an MultiFieldIndexedElement.
Expand source code
class NamedSubField(SubField): """A field to hold the value on an MultiFieldIndexedElement.""" value_cls = str def __init__(self, *args, **kwargs): self.field_uri = kwargs.pop('field_uri') if ':' in self.field_uri: raise ValueError("'field_uri' value must not contain a colon") super().__init__(*args, **kwargs) def from_xml(self, elem, account): field_elem = elem.find(self.response_tag()) val = None if field_elem is None else field_elem.text or None if val is not None: return val return self.default def to_xml(self, value, version): field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version) def field_uri_xml(self, field_uri, label): from .properties import IndexedFieldURI return IndexedFieldURI(field_uri='%s:%s' % (field_uri, self.field_uri), field_index=label).to_xml(version=None) def request_tag(self): return 't:%s' % self.field_uri def response_tag(self): return '{%s}%s' % (self.namespace, self.field_uri)
Ancestors
Methods
def field_uri_xml(self, field_uri, label)
-
Expand source code
def field_uri_xml(self, field_uri, label): from .properties import IndexedFieldURI return IndexedFieldURI(field_uri='%s:%s' % (field_uri, self.field_uri), field_index=label).to_xml(version=None)
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): field_elem = elem.find(self.response_tag()) val = None if field_elem is None else field_elem.text or None if val is not None: return val return self.default
def request_tag(self)
-
Expand source code
def request_tag(self): return 't:%s' % self.field_uri
def response_tag(self)
-
Expand source code
def response_tag(self): return '{%s}%s' % (self.namespace, self.field_uri)
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): field_elem = create_element(self.request_tag()) return set_xml_value(field_elem, value, version=version)
Inherited members
class OccurrenceField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class OccurrenceField(EWSElementField): is_complex = True
Ancestors
Subclasses
Class variables
var is_complex
class OccurrenceListField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class OccurrenceListField(OccurrenceField): is_list = True
Ancestors
Class variables
var is_list
class OnOffField (*args, **kwargs)
-
A field that handles boolean values that are On/Off instead of True/False.
Expand source code
class OnOffField(BooleanField): """A field that handles boolean values that are On/Off instead of True/False."""
Ancestors
Inherited members
class PermissionSetField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class PermissionSetField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .properties import PermissionSet kwargs['value_cls'] = PermissionSet super().__init__(*args, **kwargs)
Ancestors
Class variables
var is_complex
class PersonaPhoneNumberField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class PersonaPhoneNumberField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .properties import PhoneNumber kwargs['value_cls'] = PhoneNumber super().__init__(*args, **kwargs)
Ancestors
Class variables
var is_complex
class PhoneNumberAttributedValueField (*args, **kwargs)
-
Like EWSElementField, but for lists of EWSElement objects.
Expand source code
class PhoneNumberAttributedValueField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import PhoneNumberAttributedValue kwargs['value_cls'] = PhoneNumberAttributedValue super().__init__(*args, **kwargs)
Ancestors
class PhoneNumberField (*args, **kwargs)
-
A base class for all indexed fields.
Expand source code
class PhoneNumberField(IndexedField): is_list = True PARENT_ELEMENT_NAME = 'PhoneNumbers' def __init__(self, *args, **kwargs): from .indexed_properties import PhoneNumber kwargs['value_cls'] = PhoneNumber super().__init__(*args, **kwargs)
Ancestors
Class variables
var PARENT_ELEMENT_NAME
var is_list
class PhysicalAddressField (*args, **kwargs)
-
A base class for all indexed fields.
Expand source code
class PhysicalAddressField(IndexedField): is_list = True PARENT_ELEMENT_NAME = 'PhysicalAddresses' def __init__(self, *args, **kwargs): from .indexed_properties import PhysicalAddress kwargs['value_cls'] = PhysicalAddress super().__init__(*args, **kwargs)
Ancestors
Class variables
var PARENT_ELEMENT_NAME
var is_list
class PostalAddressAttributedValueField (*args, **kwargs)
-
Like EWSElementField, but for lists of EWSElement objects.
Expand source code
class PostalAddressAttributedValueField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import PostalAddressAttributedValue kwargs['value_cls'] = PostalAddressAttributedValue super().__init__(*args, **kwargs)
Ancestors
class ProtocolListField (*args, **kwargs)
-
Like EWSElementField, but for lists of EWSElement objects.
Expand source code
class ProtocolListField(EWSElementListField): # There is not containing element for this field. Just multiple 'Protocol' elements on the 'Account' element. def __init__(self, *args, **kwargs): from .autodiscover.properties import Protocol kwargs['value_cls'] = Protocol super().__init__(*args, **kwargs) def from_xml(self, elem, account): return [self.value_cls.from_xml(elem=e, account=account) for e in elem.findall(self.value_cls.response_tag())]
Ancestors
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): return [self.value_cls.from_xml(elem=e, account=account) for e in elem.findall(self.value_cls.response_tag())]
class RecipientAddressField (*args, **kwargs)
-
A base class for EWSElement classes that have an 'email_address' field that we want to provide helpers for.
Expand source code
class RecipientAddressField(BaseEmailField): def __init__(self, *args, **kwargs): from .properties import RecipientAddress kwargs['value_cls'] = RecipientAddress super().__init__(*args, **kwargs)
Ancestors
class RecurrenceField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class RecurrenceField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .recurrence import Recurrence kwargs['value_cls'] = Recurrence super().__init__(*args, **kwargs) def to_xml(self, value, version): return value.to_xml(version=version)
Ancestors
Class variables
var is_complex
Methods
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): return value.to_xml(version=version)
class ReferenceItemIdField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class ReferenceItemIdField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .properties import ReferenceItemId kwargs['value_cls'] = ReferenceItemId super().__init__(*args, **kwargs) def to_xml(self, value, version): return value.to_xml(version=version)
Ancestors
Class variables
var is_complex
Methods
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): return value.to_xml(version=version)
class RoutingTypeField (*args, **kwargs)
-
Like CharField, but restricts the value to a limited set of strings.
Expand source code
class RoutingTypeField(ChoiceField): def __init__(self, *args, **kwargs): kwargs['choices'] = {Choice('SMTP'), Choice('EX')} kwargs['default'] = 'SMTP' super().__init__(*args, **kwargs)
Ancestors
Inherited members
class StringAttributedValueField (*args, **kwargs)
-
Like EWSElementField, but for lists of EWSElement objects.
Expand source code
class StringAttributedValueField(EWSElementListField): def __init__(self, *args, **kwargs): from .properties import StringAttributedValue kwargs['value_cls'] = StringAttributedValue super().__init__(*args, **kwargs)
Ancestors
class SubField (name=None, is_required=False, is_required_after_save=False, is_read_only=False, is_read_only_after_send=False, is_searchable=True, is_attribute=False, default=None, supported_from=None, deprecated_from=None)
-
A field to hold the value on an IndexedElement.
Expand source code
class SubField(Field): """A field to hold the value on an IndexedElement.""" namespace = TNS value_cls = str def from_xml(self, elem, account): return elem.text def to_xml(self, value, version): return value @staticmethod def field_uri_xml(field_uri, label): from .properties import IndexedFieldURI return IndexedFieldURI(field_uri=field_uri, field_index=label).to_xml(version=None) def clean(self, value, version=None): value = super().clean(value, version=version) if self.is_required and not value: raise ValueError('Value for subfield %r must be non-empty' % self.name) return value def __hash__(self): return hash(self.name)
Ancestors
Subclasses
Class variables
var namespace
var value_cls
-
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
Static methods
def field_uri_xml(field_uri, label)
-
Expand source code
@staticmethod def field_uri_xml(field_uri, label): from .properties import IndexedFieldURI return IndexedFieldURI(field_uri=field_uri, field_index=label).to_xml(version=None)
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): value = super().clean(value, version=version) if self.is_required and not value: raise ValueError('Value for subfield %r must be non-empty' % self.name) return value
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): return elem.text
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): return value
class TaskRecurrenceField (*args, **kwargs)
-
A generic field for any EWSElement object.
Expand source code
class TaskRecurrenceField(EWSElementField): is_complex = True def __init__(self, *args, **kwargs): from .recurrence import TaskRecurrence kwargs['value_cls'] = TaskRecurrence super().__init__(*args, **kwargs) def to_xml(self, value, version): return value.to_xml(version=version)
Ancestors
Class variables
var is_complex
Methods
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): return value.to_xml(version=version)
class TextField (*args, **kwargs)
-
A field that stores a string value with no length limit.
Expand source code
class TextField(FieldURIField): """A field that stores a string value with no length limit.""" value_cls = str is_complex = True
Ancestors
Subclasses
Class variables
var is_complex
var value_cls
-
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
class TextListField (*args, **kwargs)
-
Like TextField, but for lists of text.
Expand source code
class TextListField(TextField): """Like TextField, but for lists of text.""" is_list = True def from_xml(self, elem, account): iter_elem = elem.find(self.response_tag()) if iter_elem is not None: return get_xml_attrs(iter_elem, '{%s}String' % TNS) return self.default
Ancestors
Class variables
var is_list
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): iter_elem = elem.find(self.response_tag()) if iter_elem is not None: return get_xml_attrs(iter_elem, '{%s}String' % TNS) return self.default
Inherited members
class TimeField (*args, **kwargs)
-
A field that handles time values.
Expand source code
class TimeField(FieldURIField): """A field that handles time values.""" value_cls = datetime.time def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: if ':' in val: # Assume a string of the form HH:MM:SS return datetime.datetime.strptime(val, '%H:%M:%S').time() # Assume an integer in minutes since midnight return (datetime.datetime(2000, 1, 1) + datetime.timedelta(minutes=int(val))).time() except ValueError: pass return self.default
Ancestors
Class variables
var value_cls
-
time([hour[, minute[, second[, microsecond[, tzinfo]]]]]) –> a time object
All arguments are optional. tzinfo may be None, or an instance of a tzinfo subclass. The remaining arguments may be ints.
Methods
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): val = self._get_val_from_elem(elem) if val is not None: try: if ':' in val: # Assume a string of the form HH:MM:SS return datetime.datetime.strptime(val, '%H:%M:%S').time() # Assume an integer in minutes since midnight return (datetime.datetime(2000, 1, 1) + datetime.timedelta(minutes=int(val))).time() except ValueError: pass return self.default
class TimeZoneField (*args, **kwargs)
-
A field that handles timezone values.
Expand source code
class TimeZoneField(FieldURIField): """A field that handles timezone values.""" value_cls = EWSTimeZone def clean(self, value, version=None): # Allow other timezone implementations as input if value is not None: value = self.value_cls.from_timezone(value) return super().clean(value=value, version=version) def from_xml(self, elem, account): field_elem = elem.find(self.response_tag()) if field_elem is not None: ms_id = field_elem.get('Id') ms_name = field_elem.get('Name') try: return self.value_cls.from_ms_id(ms_id or ms_name) except UnknownTimeZone: log.warning( "Cannot convert value '%s' on field '%s' to type %s (unknown timezone ID)", (ms_id or ms_name), self.name, self.value_cls ) return None return self.default def to_xml(self, value, version): attrs = OrderedDict([('Id', value.ms_id)]) if value.ms_name: attrs['Name'] = value.ms_name return create_element(self.request_tag(), attrs=attrs)
Ancestors
Class variables
var value_cls
-
Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by services.GetServerTimeZones.
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): # Allow other timezone implementations as input if value is not None: value = self.value_cls.from_timezone(value) return super().clean(value=value, version=version)
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): field_elem = elem.find(self.response_tag()) if field_elem is not None: ms_id = field_elem.get('Id') ms_name = field_elem.get('Name') try: return self.value_cls.from_ms_id(ms_id or ms_name) except UnknownTimeZone: log.warning( "Cannot convert value '%s' on field '%s' to type %s (unknown timezone ID)", (ms_id or ms_name), self.name, self.value_cls ) return None return self.default
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): attrs = OrderedDict([('Id', value.ms_id)]) if value.ms_name: attrs['Name'] = value.ms_name return create_element(self.request_tag(), attrs=attrs)
class TypeValueField (*args, **kwargs)
-
This field type has no value_cls because values may have many different types.
Expand source code
class TypeValueField(FieldURIField): """This field type has no value_cls because values may have many different types.""" TYPES_MAP = { 'Boolean': bool, 'Integer32': int, 'UnsignedInteger32': int, 'Integer64': int, 'UnsignedInteger64': int, # Python doesn't have a single-byte type to represent 'Byte' 'ByteArray': bytes, 'String': str, 'StringArray': str, # A list of strings 'DateTime': EWSDateTime, } TYPES_MAP_REVERSED = { bool: 'Boolean', int: 'Integer64', # Python doesn't have a single-byte type to represent 'Byte' bytes: 'ByteArray', str: 'String', datetime.datetime: 'DateTime', EWSDateTime: 'DateTime', } @classmethod def get_type(cls, value): if isinstance(value, bytes) and len(value) == 1: # This is a single byte. Translate it to the 'Byte' type return 'Byte' if is_iterable(value): # We don't allow generators as values, so keep the logic simple try: first = next(iter(value)) except StopIteration: first = None value_type = '%sArray' % cls.TYPES_MAP_REVERSED[type(first)] if value_type not in cls.TYPES_MAP: raise ValueError('%r is not a supported type' % value) return value_type return cls.TYPES_MAP_REVERSED[type(value)] @classmethod def is_array_type(cls, value_type): return value_type == 'StringArray' def clean(self, value, version=None): if value is None: if self.is_required and self.default is None: raise ValueError("'%s' is a required field with no default" % self.name) return self.default return value def from_xml(self, elem, account): field_elem = elem.find(self.response_tag()) if field_elem is None: return self.default value_type_str = get_xml_attr(field_elem, '{%s}Type' % TNS) value = get_xml_attr(field_elem, '{%s}Value' % TNS) if value_type_str == 'Byte': try: # The value is an unsigned integer in the range 0 -> 255. Convert it to a single byte return xml_text_to_value(value, int).to_bytes(1, 'little', signed=False) except OverflowError as e: log.warning('Invalid byte value %r (%e)', value, e) return None value_type = self.TYPES_MAP[value_type_str] if self. is_array_type(value_type_str): return tuple(xml_text_to_value(value=v, value_type=value_type) for v in value.split(' ')) return xml_text_to_value(value=value, value_type=value_type) def to_xml(self, value, version): value_type_str = self.get_type(value) if value_type_str == 'Byte': # A single byte is encoded to an unsigned integer in the range 0 -> 255 value = int.from_bytes(value, byteorder='little', signed=False) elif is_iterable(value): value = ' '.join(value_to_xml_text(v) for v in value) field_elem = create_element(self.request_tag()) field_elem.append(set_xml_value(create_element('t:Type'), value_type_str, version=version)) field_elem.append(set_xml_value(create_element('t:Value'), value, version=version)) return field_elem
Ancestors
Class variables
var TYPES_MAP
var TYPES_MAP_REVERSED
Static methods
def get_type(value)
-
Expand source code
@classmethod def get_type(cls, value): if isinstance(value, bytes) and len(value) == 1: # This is a single byte. Translate it to the 'Byte' type return 'Byte' if is_iterable(value): # We don't allow generators as values, so keep the logic simple try: first = next(iter(value)) except StopIteration: first = None value_type = '%sArray' % cls.TYPES_MAP_REVERSED[type(first)] if value_type not in cls.TYPES_MAP: raise ValueError('%r is not a supported type' % value) return value_type return cls.TYPES_MAP_REVERSED[type(value)]
def is_array_type(value_type)
-
Expand source code
@classmethod def is_array_type(cls, value_type): return value_type == 'StringArray'
Methods
def clean(self, value, version=None)
-
Expand source code
def clean(self, value, version=None): if value is None: if self.is_required and self.default is None: raise ValueError("'%s' is a required field with no default" % self.name) return self.default return value
def from_xml(self, elem, account)
-
Expand source code
def from_xml(self, elem, account): field_elem = elem.find(self.response_tag()) if field_elem is None: return self.default value_type_str = get_xml_attr(field_elem, '{%s}Type' % TNS) value = get_xml_attr(field_elem, '{%s}Value' % TNS) if value_type_str == 'Byte': try: # The value is an unsigned integer in the range 0 -> 255. Convert it to a single byte return xml_text_to_value(value, int).to_bytes(1, 'little', signed=False) except OverflowError as e: log.warning('Invalid byte value %r (%e)', value, e) return None value_type = self.TYPES_MAP[value_type_str] if self. is_array_type(value_type_str): return tuple(xml_text_to_value(value=v, value_type=value_type) for v in value.split(' ')) return xml_text_to_value(value=value, value_type=value_type)
def to_xml(self, value, version)
-
Expand source code
def to_xml(self, value, version): value_type_str = self.get_type(value) if value_type_str == 'Byte': # A single byte is encoded to an unsigned integer in the range 0 -> 255 value = int.from_bytes(value, byteorder='little', signed=False) elif is_iterable(value): value = ' '.join(value_to_xml_text(v) for v in value) field_elem = create_element(self.request_tag()) field_elem.append(set_xml_value(create_element('t:Type'), value_type_str, version=version)) field_elem.append(set_xml_value(create_element('t:Value'), value, version=version)) return field_elem
class URIField (*args, **kwargs)
-
Helper to mark strings that must conform to xsd:anyURI. If we want an URI validator, see http://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuri
Expand source code
class URIField(TextField): """Helper to mark strings that must conform to xsd:anyURI. If we want an URI validator, see http://stackoverflow.com/questions/14466585/is-this-regex-correct-for-xsdanyuri """
Ancestors
Inherited members
class UnknownEntriesField (*args, **kwargs)
-
Like CharField, but for lists of strings.
Expand source code
class UnknownEntriesField(CharListField): def list_elem_tag(self): return '{%s}UnknownEntry' % self.namespace
Ancestors
Methods
def list_elem_tag(self)
-
Expand source code
def list_elem_tag(self): return '{%s}UnknownEntry' % self.namespace
Inherited members