diff --git a/CHANGELOG.md b/CHANGELOG.md index accef897..910463cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Change Log HEAD ---- + + +3.3.0 +----- - Switch `EWSTimeZone` to be implemented on top of the new `zoneinfo` module in Python 3.9 instead of `pytz`. `backports.zoneinfo` is used for earlier versions of Python. This means that the `ÈWSTimeZone` methods `timezone()`, `normalize()` and `localize()` methods are now deprecated. diff --git a/docs/exchangelib/account.html b/docs/exchangelib/account.html index c36c33ed..54da4c09 100644 --- a/docs/exchangelib/account.html +++ b/docs/exchangelib/account.html @@ -3,16 +3,16 @@ - + exchangelib.account API documentation - + - + @@ -2992,7 +2992,7 @@

-

Generated by pdoc 0.8.4.

+

Generated by pdoc 0.9.1.

\ No newline at end of file diff --git a/docs/exchangelib/attachments.html b/docs/exchangelib/attachments.html index 03215337..09137298 100644 --- a/docs/exchangelib/attachments.html +++ b/docs/exchangelib/attachments.html @@ -3,16 +3,16 @@ - + exchangelib.attachments API documentation - + - + @@ -1180,7 +1180,7 @@

-

Generated by pdoc 0.8.4.

+

Generated by pdoc 0.9.1.

\ No newline at end of file diff --git a/docs/exchangelib/autodiscover/cache.html b/docs/exchangelib/autodiscover/cache.html index 3315244d..166d45b4 100644 --- a/docs/exchangelib/autodiscover/cache.html +++ b/docs/exchangelib/autodiscover/cache.html @@ -3,16 +3,16 @@ - + exchangelib.autodiscover.cache API documentation - + - + @@ -69,6 +69,13 @@

Module exchangelib.autodiscover.cache

# We don't know which file caused the error, so just delete them all. try: shelve_handle = shelve.open(filename) + # Try to actually use the shelve. Some implementations may allow opening the file but then throw + # errors on access. + try: + _ = shelve_handle[''] + except KeyError: + # The entry doesn't exist. This is expected. + pass except Exception as e: for f in glob.glob(filename + '*'): log.warning('Deleting invalid cache file %s (%r)', f, e) @@ -230,6 +237,13 @@

Functions

# We don't know which file caused the error, so just delete them all. try: shelve_handle = shelve.open(filename) + # Try to actually use the shelve. Some implementations may allow opening the file but then throw + # errors on access. + try: + _ = shelve_handle[''] + except KeyError: + # The entry doesn't exist. This is expected. + pass except Exception as e: for f in glob.glob(filename + '*'): log.warning('Deleting invalid cache file %s (%r)', f, e) @@ -435,7 +449,7 @@

-

Generated by pdoc 0.8.4.

+

Generated by pdoc 0.9.1.

\ No newline at end of file diff --git a/docs/exchangelib/autodiscover/discovery.html b/docs/exchangelib/autodiscover/discovery.html index 47d9c4b9..6a553917 100644 --- a/docs/exchangelib/autodiscover/discovery.html +++ b/docs/exchangelib/autodiscover/discovery.html @@ -3,16 +3,16 @@ - + exchangelib.autodiscover.discovery API documentation - + - + @@ -30,6 +30,7 @@

Module exchangelib.autodiscover.discovery

import time from urllib.parse import urlparse +from cached_property import threaded_cached_property import dns.resolver from ..configuration import Configuration @@ -38,7 +39,7 @@

Module exchangelib.autodiscover.discovery

from ..protocol import Protocol, FailFast from ..transport import get_auth_method_from_response, DEFAULT_HEADERS, NOAUTH, OAUTH2, CREDENTIALS_REQUIRED from ..util import post_ratelimited, get_domain, get_redirect_url, _back_off_if_needed, _may_retry_on_error, \ - is_valid_hostname, DummyResponse, CONNECTION_ERRORS, TLS_ERRORS + DummyResponse, CONNECTION_ERRORS, TLS_ERRORS from ..version import Version from .cache import autodiscover_cache from .properties import Autodiscover @@ -173,8 +174,14 @@

Module exchangelib.autodiscover.discovery

domain = get_domain(self.email) return domain, self.credentials + @threaded_cached_property + def resolver(self): + resolver = dns.resolver.Resolver() + resolver.timeout = AutodiscoverProtocol.TIMEOUT + return resolver + def _build_response(self, ad_response): - ews_url = ad_response.protocol.ews_url + ews_url = ad_response.ews_url if not ews_url: raise AutoDiscoverFailed("Response is missing an 'ews_url' value") if not ad_response.autodiscover_smtp_address: @@ -269,7 +276,7 @@

Module exchangelib.autodiscover.discovery

""" # We are connecting to untrusted servers here, so take necessary precautions. hostname = urlparse(url).netloc - if not is_valid_hostname(hostname, timeout=AutodiscoverProtocol.TIMEOUT): + if not self._is_valid_hostname(hostname): # 'requests' is really bad at reporting that a hostname cannot be resolved. Let's check this separately. # Don't retry on DNS errors. They will most likely be persistent. raise TransportError('%r has no DNS entry' % hostname) @@ -300,9 +307,8 @@

Module exchangelib.autodiscover.discovery

self.INITIAL_RETRY_POLICY.back_off(self.RETRY_WAIT) retry += 1 continue - else: - log.debug("Connection error on URL %s: %s", url, e) - raise TransportError(str(e)) + log.debug("Connection error on URL %s: %s", url, e) + raise TransportError(str(e)) try: auth_type = get_auth_method_from_response(response=r) except UnauthorizedError: @@ -391,6 +397,47 @@

Module exchangelib.autodiscover.discovery

log.debug('Invalid response: %s', e) return False, None + def _is_valid_hostname(self, hostname): + log.debug('Checking if %s can be looked up in DNS', hostname) + try: + self.resolver.resolve(hostname) + except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + return False + return True + + def _get_srv_records(self, hostname): + """Send a DNS query for SRV entries for the hostname. + + An SRV entry that has been formatted for autodiscovery will have the following format: + + canonical name = mail.example.com. + service = 8 100 443 webmail.example.com. + + The first three numbers in the service line are: priority, weight, port + + Args: + hostname: + + """ + log.debug('Attempting to get SRV records for %s', hostname) + records = [] + try: + answers = self.resolver.resolve('%s.' % hostname, 'SRV') + except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e: + log.debug('DNS lookup failure: %s', e) + return records + for rdata in answers: + try: + vals = rdata.to_text().strip().rstrip('.').split(' ') + # Raise ValueError if the first three are not ints, and IndexError if there are less than 4 values + priority, weight, port, srv = int(vals[0]), int(vals[1]), int(vals[2]), vals[3] + record = SrvRecord(priority=priority, weight=weight, port=port, srv=srv) + log.debug('Found SRV record %s ', record) + records.append(record) + except (ValueError, IndexError): + log.debug('Incompatible SRV record for %s (%s)', hostname, rdata.to_text()) + return records + def _step_1(self, hostname): """The client sends an Autodiscover request to https://example.com/autodiscover/autodiscover.xml and then does one of the following: @@ -482,7 +529,7 @@

Module exchangelib.autodiscover.discovery

""" dns_hostname = '_autodiscover._tcp.%s' % hostname log.info('Step 4: Trying autodiscover on %r with email %r', dns_hostname, self.email) - srv_records = _get_srv_records(dns_hostname) + srv_records = self._get_srv_records(dns_hostname) try: srv_host = _select_srv_host(srv_records) except ValueError: @@ -551,42 +598,6 @@

Module exchangelib.autodiscover.discovery

'an official test at https://testconnectivity.microsoft.com' % self.email) -def _get_srv_records(hostname): - """Send a DNS query for SRV entries for the hostname. - - An SRV entry that has been formatted for autodiscovery will have the following format: - - canonical name = mail.example.com. - service = 8 100 443 webmail.example.com. - - The first three numbers in the service line are: priority, weight, port - - Args: - hostname: - - """ - log.debug('Attempting to get SRV records for %s', hostname) - resolver = dns.resolver.Resolver() - resolver.timeout = AutodiscoverProtocol.TIMEOUT - records = [] - try: - answers = resolver.query('%s.' % hostname, 'SRV') - except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e: - log.debug('DNS lookup failure: %s', e) - return records - for rdata in answers: - try: - vals = rdata.to_text().strip().rstrip('.').split(' ') - # Raise ValueError if the first three are not ints, and IndexError if there are less than 4 values - priority, weight, port, srv = int(vals[0]), int(vals[1]), int(vals[2]), vals[3] - record = SrvRecord(priority=priority, weight=weight, port=port, srv=srv) - log.debug('Found SRV record %s ', record) - records.append(record) - except (ValueError, IndexError): - log.debug('Incompatible SRV record for %s (%s)', hostname, rdata.to_text()) - return records - - def _select_srv_host(srv_records): """Select the record with the highest priority, that also supports TLS @@ -775,8 +786,14 @@

Args

domain = get_domain(self.email) return domain, self.credentials + @threaded_cached_property + def resolver(self): + resolver = dns.resolver.Resolver() + resolver.timeout = AutodiscoverProtocol.TIMEOUT + return resolver + def _build_response(self, ad_response): - ews_url = ad_response.protocol.ews_url + ews_url = ad_response.ews_url if not ews_url: raise AutoDiscoverFailed("Response is missing an 'ews_url' value") if not ad_response.autodiscover_smtp_address: @@ -871,7 +888,7 @@

Args

""" # We are connecting to untrusted servers here, so take necessary precautions. hostname = urlparse(url).netloc - if not is_valid_hostname(hostname, timeout=AutodiscoverProtocol.TIMEOUT): + if not self._is_valid_hostname(hostname): # 'requests' is really bad at reporting that a hostname cannot be resolved. Let's check this separately. # Don't retry on DNS errors. They will most likely be persistent. raise TransportError('%r has no DNS entry' % hostname) @@ -902,9 +919,8 @@

Args

self.INITIAL_RETRY_POLICY.back_off(self.RETRY_WAIT) retry += 1 continue - else: - log.debug("Connection error on URL %s: %s", url, e) - raise TransportError(str(e)) + log.debug("Connection error on URL %s: %s", url, e) + raise TransportError(str(e)) try: auth_type = get_auth_method_from_response(response=r) except UnauthorizedError: @@ -993,6 +1009,47 @@

Args

log.debug('Invalid response: %s', e) return False, None + def _is_valid_hostname(self, hostname): + log.debug('Checking if %s can be looked up in DNS', hostname) + try: + self.resolver.resolve(hostname) + except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + return False + return True + + def _get_srv_records(self, hostname): + """Send a DNS query for SRV entries for the hostname. + + An SRV entry that has been formatted for autodiscovery will have the following format: + + canonical name = mail.example.com. + service = 8 100 443 webmail.example.com. + + The first three numbers in the service line are: priority, weight, port + + Args: + hostname: + + """ + log.debug('Attempting to get SRV records for %s', hostname) + records = [] + try: + answers = self.resolver.resolve('%s.' % hostname, 'SRV') + except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e: + log.debug('DNS lookup failure: %s', e) + return records + for rdata in answers: + try: + vals = rdata.to_text().strip().rstrip('.').split(' ') + # Raise ValueError if the first three are not ints, and IndexError if there are less than 4 values + priority, weight, port, srv = int(vals[0]), int(vals[1]), int(vals[2]), vals[3] + record = SrvRecord(priority=priority, weight=weight, port=port, srv=srv) + log.debug('Found SRV record %s ', record) + records.append(record) + except (ValueError, IndexError): + log.debug('Incompatible SRV record for %s (%s)', hostname, rdata.to_text()) + return records + def _step_1(self, hostname): """The client sends an Autodiscover request to https://example.com/autodiscover/autodiscover.xml and then does one of the following: @@ -1084,7 +1141,7 @@

Args

""" dns_hostname = '_autodiscover._tcp.%s' % hostname log.info('Step 4: Trying autodiscover on %r with email %r', dns_hostname, self.email) - srv_records = _get_srv_records(dns_hostname) + srv_records = self._get_srv_records(dns_hostname) try: srv_host = _select_srv_host(srv_records) except ValueError: @@ -1167,6 +1224,32 @@

Class variables

+

Instance variables

+
+
var resolver
+
+
+
+ +Expand source code + +
def __get__(self, obj, cls):
+    if obj is None:
+        return self
+
+    obj_dict = obj.__dict__
+    name = self.func.__name__
+    with self.lock:
+        try:
+            # check if the value was computed before the lock was acquired
+            return obj_dict[name]
+
+        except KeyError:
+            # if not, do the calculation and release the lock
+            return obj_dict.setdefault(name, self.func(obj))
+
+
+

Methods

@@ -1290,6 +1373,7 @@

RETRY_WAIT
  • clear
  • discover
  • +
  • resolver
  • @@ -1301,7 +1385,7 @@

    -

    Generated by pdoc 0.8.4.

    +

    Generated by pdoc 0.9.1.

    \ No newline at end of file diff --git a/docs/exchangelib/autodiscover/index.html b/docs/exchangelib/autodiscover/index.html index fd6d1634..02de9911 100644 --- a/docs/exchangelib/autodiscover/index.html +++ b/docs/exchangelib/autodiscover/index.html @@ -3,16 +3,16 @@ - + exchangelib.autodiscover API documentation - + - + @@ -463,8 +463,14 @@

    Args

    domain = get_domain(self.email) return domain, self.credentials + @threaded_cached_property + def resolver(self): + resolver = dns.resolver.Resolver() + resolver.timeout = AutodiscoverProtocol.TIMEOUT + return resolver + def _build_response(self, ad_response): - ews_url = ad_response.protocol.ews_url + ews_url = ad_response.ews_url if not ews_url: raise AutoDiscoverFailed("Response is missing an 'ews_url' value") if not ad_response.autodiscover_smtp_address: @@ -559,7 +565,7 @@

    Args

    """ # We are connecting to untrusted servers here, so take necessary precautions. hostname = urlparse(url).netloc - if not is_valid_hostname(hostname, timeout=AutodiscoverProtocol.TIMEOUT): + if not self._is_valid_hostname(hostname): # 'requests' is really bad at reporting that a hostname cannot be resolved. Let's check this separately. # Don't retry on DNS errors. They will most likely be persistent. raise TransportError('%r has no DNS entry' % hostname) @@ -590,9 +596,8 @@

    Args

    self.INITIAL_RETRY_POLICY.back_off(self.RETRY_WAIT) retry += 1 continue - else: - log.debug("Connection error on URL %s: %s", url, e) - raise TransportError(str(e)) + log.debug("Connection error on URL %s: %s", url, e) + raise TransportError(str(e)) try: auth_type = get_auth_method_from_response(response=r) except UnauthorizedError: @@ -681,6 +686,47 @@

    Args

    log.debug('Invalid response: %s', e) return False, None + def _is_valid_hostname(self, hostname): + log.debug('Checking if %s can be looked up in DNS', hostname) + try: + self.resolver.resolve(hostname) + except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + return False + return True + + def _get_srv_records(self, hostname): + """Send a DNS query for SRV entries for the hostname. + + An SRV entry that has been formatted for autodiscovery will have the following format: + + canonical name = mail.example.com. + service = 8 100 443 webmail.example.com. + + The first three numbers in the service line are: priority, weight, port + + Args: + hostname: + + """ + log.debug('Attempting to get SRV records for %s', hostname) + records = [] + try: + answers = self.resolver.resolve('%s.' % hostname, 'SRV') + except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e: + log.debug('DNS lookup failure: %s', e) + return records + for rdata in answers: + try: + vals = rdata.to_text().strip().rstrip('.').split(' ') + # Raise ValueError if the first three are not ints, and IndexError if there are less than 4 values + priority, weight, port, srv = int(vals[0]), int(vals[1]), int(vals[2]), vals[3] + record = SrvRecord(priority=priority, weight=weight, port=port, srv=srv) + log.debug('Found SRV record %s ', record) + records.append(record) + except (ValueError, IndexError): + log.debug('Incompatible SRV record for %s (%s)', hostname, rdata.to_text()) + return records + def _step_1(self, hostname): """The client sends an Autodiscover request to https://example.com/autodiscover/autodiscover.xml and then does one of the following: @@ -772,7 +818,7 @@

    Args

    """ dns_hostname = '_autodiscover._tcp.%s' % hostname log.info('Step 4: Trying autodiscover on %r with email %r', dns_hostname, self.email) - srv_records = _get_srv_records(dns_hostname) + srv_records = self._get_srv_records(dns_hostname) try: srv_host = _select_srv_host(srv_records) except ValueError: @@ -855,6 +901,32 @@

    Class variables

  • +

    Instance variables

    +
    +
    var resolver
    +
    +
    +
    + +Expand source code + +
    def __get__(self, obj, cls):
    +    if obj is None:
    +        return self
    +
    +    obj_dict = obj.__dict__
    +    name = self.func.__name__
    +    with self.lock:
    +        try:
    +            # check if the value was computed before the lock was acquired
    +            return obj_dict[name]
    +
    +        except KeyError:
    +            # if not, do the calculation and release the lock
    +            return obj_dict.setdefault(name, self.func(obj))
    +
    +
    +

    Methods

    @@ -975,6 +1047,7 @@

    RETRY_WAIT
  • clear
  • discover
  • +
  • resolver
  • @@ -983,7 +1056,7 @@

    -

    Generated by pdoc 0.8.4.

    +

    Generated by pdoc 0.9.1.

    \ No newline at end of file diff --git a/docs/exchangelib/autodiscover/properties.html b/docs/exchangelib/autodiscover/properties.html index dc71148c..2a21c24e 100644 --- a/docs/exchangelib/autodiscover/properties.html +++ b/docs/exchangelib/autodiscover/properties.html @@ -3,16 +3,16 @@ - + exchangelib.autodiscover.properties API documentation - + - + @@ -66,6 +66,7 @@

    Module exchangelib.autodiscover.properties

    class MailStore(IntExtUrlBase): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailstore-pox""" ELEMENT_NAME = 'MailStore' __slots__ = tuple() @@ -89,10 +90,13 @@

    Module exchangelib.autodiscover.properties

    """ ELEMENT_NAME = 'Protocol' + WEB = 'WEB' + EXCH = 'EXCH' + EXPR = 'EXPR' + EXHTTP = 'EXHTTP' + TYPES = (WEB, EXCH, EXPR, EXHTTP) FIELDS = Fields( - ChoiceField('type', field_uri='Type', choices={ - Choice('WEB'), Choice('EXCH'), Choice('EXPR'), Choice('EXHTTP') - }, namespace=RNS), + ChoiceField('type', field_uri='Type', choices={Choice(c) for c in TYPES}, namespace=RNS), TextField('as_url', field_uri='ASUrl', namespace=RNS), ) __slots__ = tuple(f.name for f in FIELDS) @@ -100,7 +104,8 @@

    Module exchangelib.autodiscover.properties

    class IntExtBase(AutodiscoverBase): FIELDS = Fields( - # TODO: 'OWAUrl' also has an AuthenticationMethod enum-style XML attribute + # TODO: 'OWAUrl' also has an AuthenticationMethod enum-style XML attribute with values: + # WindowsIntegrated, FBA, NTLM, Digest, Basic TextField('owa_url', field_uri='OWAUrl', namespace=RNS), EWSElementField('protocol', value_cls=SimpleProtocol), ) @@ -120,16 +125,14 @@

    Module exchangelib.autodiscover.properties

    __slots__ = tuple() -class Protocol(AutodiscoverBase): +class Protocol(SimpleProtocol): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox""" - ELEMENT_NAME = 'Protocol' - TYPES = ('WEB', 'EXCH', 'EXPR', 'EXHTTP') FIELDS = Fields( # Attribute 'Type' is ignored here. Has a name conflict with the child element and does not seem useful. TextField('version', field_uri='Version', is_attribute=True, namespace=RNS), - ChoiceField('type', field_uri='Type', namespace=RNS, choices={Choice(p) for p in TYPES}), - TextField('internal', field_uri='Internal', namespace=RNS), - TextField('external', field_uri='External', namespace=RNS), + ChoiceField('type', field_uri='Type', namespace=RNS, choices={Choice(p) for p in SimpleProtocol.TYPES}), + EWSElementField('internal', field_uri='Internal', value_cls=Internal), + EWSElementField('external', field_uri='External', value_cls=External), IntegerField('ttl', field_uri='TTL', namespace=RNS, default=1), # TTL for this autodiscover response, in hours TextField('server', field_uri='Server', namespace=RNS), TextField('server_dn', field_uri='ServerDN', namespace=RNS), @@ -289,17 +292,25 @@

    Module exchangelib.autodiscover.properties

    return None @property - def protocol(self): - # There are three possible protocol types: EXCH, EXPR and WEB. EXPR is meant for EWS. See - # https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16 - # We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available. - protocols = {p.type: p for p in self.account.protocols} - if 'EXPR' in protocols: - return protocols['EXPR'] - if 'EXCH' in protocols: - return protocols['EXCH'] - # Neither type was found. Give up - raise ValueError('No valid protocols in response: %s' % self.account.protocols) + def ews_url(self): + """Return the EWS URL contained in the response + + A response may contain a number of possible protocol types. EXPR is meant for EWS. See + https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16 + + We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available. + + Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if + available. + """ + protocols = {p.type: p for p in self.account.protocols if p.ews_url} + if Protocol.EXPR in protocols: + return protocols[Protocol.EXPR].ews_url + if Protocol.EXCH in protocols: + return protocols[Protocol.EXCH].ews_url + raise ValueError( + 'No EWS URL found in any of the available protocols: %s' % [str(p) for p in self.account.protocols] + ) class ErrorResponse(EWSElement): @@ -341,7 +352,7 @@

    Module exchangelib.autodiscover.properties

    bytes_content: """ - if not is_xml(bytes_content): + if not is_xml(bytes_content) and not is_xml(bytes_content, expected_prefix=b'<Autodiscover '): raise ValueError('Response is not XML: %s' % bytes_content) try: root = to_xml(bytes_content).getroot() @@ -614,7 +625,7 @@

    Inherited members

    bytes_content: """ - if not is_xml(bytes_content): + if not is_xml(bytes_content) and not is_xml(bytes_content, expected_prefix=b'<Autodiscover '): raise ValueError('Response is not XML: %s' % bytes_content) try: root = to_xml(bytes_content).getroot() @@ -687,7 +698,7 @@

    Args

    bytes_content: """ - if not is_xml(bytes_content): + if not is_xml(bytes_content) and not is_xml(bytes_content, expected_prefix=b'<Autodiscover '): raise ValueError('Response is not XML: %s' % bytes_content) try: root = to_xml(bytes_content).getroot() @@ -789,7 +800,6 @@

    Subclasses

  • IntExtBase
  • IntExtUrlBase
  • NetworkRequirements
  • -
  • Protocol
  • Response
  • SimpleProtocol
  • User
  • @@ -1008,7 +1018,8 @@

    Inherited members

    class IntExtBase(AutodiscoverBase):
         FIELDS = Fields(
    -        # TODO: 'OWAUrl' also has an AuthenticationMethod enum-style XML attribute
    +        # TODO: 'OWAUrl' also has an AuthenticationMethod enum-style XML attribute with values:
    +        # WindowsIntegrated, FBA, NTLM, Digest, Basic
             TextField('owa_url', field_uri='OWAUrl', namespace=RNS),
             EWSElementField('protocol', value_cls=SimpleProtocol),
         )
    @@ -1159,12 +1170,13 @@ 

    Inherited members

    (**kwargs)

    -

    Base class for all XML element implementations

    +
    Expand source code
    class MailStore(IntExtUrlBase):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailstore-pox"""
         ELEMENT_NAME = 'MailStore'
         __slots__ = tuple()
    @@ -1273,16 +1285,14 @@

    Inherited members

    Expand source code -
    class Protocol(AutodiscoverBase):
    +
    class Protocol(SimpleProtocol):
         """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox"""
    -    ELEMENT_NAME = 'Protocol'
    -    TYPES = ('WEB', 'EXCH', 'EXPR', 'EXHTTP')
         FIELDS = Fields(
             # Attribute 'Type' is ignored here. Has a name conflict with the child element and does not seem useful.
             TextField('version', field_uri='Version', is_attribute=True, namespace=RNS),
    -        ChoiceField('type', field_uri='Type', namespace=RNS, choices={Choice(p) for p in TYPES}),
    -        TextField('internal', field_uri='Internal', namespace=RNS),
    -        TextField('external', field_uri='External', namespace=RNS),
    +        ChoiceField('type', field_uri='Type', namespace=RNS, choices={Choice(p) for p in SimpleProtocol.TYPES}),
    +        EWSElementField('internal', field_uri='Internal', value_cls=Internal),
    +        EWSElementField('external', field_uri='External', value_cls=External),
             IntegerField('ttl', field_uri='TTL', namespace=RNS, default=1),  # TTL for this autodiscover response, in hours
             TextField('server', field_uri='Server', namespace=RNS),
             TextField('server_dn', field_uri='ServerDN', namespace=RNS),
    @@ -1352,23 +1362,16 @@ 

    Inherited members

    Ancestors

    Class variables

    -
    var ELEMENT_NAME
    -
    -
    -
    var FIELDS
    -
    var TYPES
    -
    -
    -

    Instance variables

    @@ -1376,10 +1379,6 @@

    Instance variables

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

    -
    var as_url
    -
    -

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

    -
    var auth_package

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

    @@ -1570,10 +1569,6 @@

    Instance variables

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

    -
    var type
    -
    -

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

    -
    var um_url

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

    @@ -1589,12 +1584,14 @@

    Instance variables

    Inherited members

    @@ -1648,17 +1645,25 @@

    Inherited members

    return None @property - def protocol(self): - # There are three possible protocol types: EXCH, EXPR and WEB. EXPR is meant for EWS. See - # https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16 - # We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available. - protocols = {p.type: p for p in self.account.protocols} - if 'EXPR' in protocols: - return protocols['EXPR'] - if 'EXCH' in protocols: - return protocols['EXCH'] - # Neither type was found. Give up - raise ValueError('No valid protocols in response: %s' % self.account.protocols)
    + def ews_url(self): + """Return the EWS URL contained in the response + + A response may contain a number of possible protocol types. EXPR is meant for EWS. See + https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16 + + We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available. + + Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if + available. + """ + protocols = {p.type: p for p in self.account.protocols if p.ews_url} + if Protocol.EXPR in protocols: + return protocols[Protocol.EXPR].ews_url + if Protocol.EXCH in protocols: + return protocols[Protocol.EXCH].ews_url + raise ValueError( + 'No EWS URL found in any of the available protocols: %s' % [str(p) for p in self.account.protocols] + )

    Ancestors

      @@ -1700,25 +1705,38 @@

      Instance variables

      return None
    -
    var protocol
    +
    var ews_url
    -
    +

    Return the EWS URL contained in the response

    +

    A response may contain a number of possible protocol types. EXPR is meant for EWS. See +https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16

    +

    We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available.

    +

    Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if +available.

    Expand source code
    @property
    -def protocol(self):
    -    # There are three possible protocol types: EXCH, EXPR and WEB. EXPR is meant for EWS. See
    -    # https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16
    -    # We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available.
    -    protocols = {p.type: p for p in self.account.protocols}
    -    if 'EXPR' in protocols:
    -        return protocols['EXPR']
    -    if 'EXCH' in protocols:
    -        return protocols['EXCH']
    -    # Neither type was found. Give up
    -    raise ValueError('No valid protocols in response: %s' % self.account.protocols)
    +def ews_url(self): + """Return the EWS URL contained in the response + + A response may contain a number of possible protocol types. EXPR is meant for EWS. See + https://techcommunity.microsoft.com/t5/blogs/blogarticleprintpage/blog-id/Exchange/article-id/16 + + We allow fallback to EXCH if EXPR is not available, to support installations where EXPR is not available. + + Additionally, some responses may contain and EXPR with no EWS URL. In that case, return the URL from EXCH, if + available. + """ + protocols = {p.type: p for p in self.account.protocols if p.ews_url} + if Protocol.EXPR in protocols: + return protocols[Protocol.EXPR].ews_url + if Protocol.EXCH in protocols: + return protocols[Protocol.EXCH].ews_url + raise ValueError( + 'No EWS URL found in any of the available protocols: %s' % [str(p) for p in self.account.protocols] + )
    var redirect_address
    @@ -1790,10 +1808,13 @@

    Inherited members

    """ ELEMENT_NAME = 'Protocol' + WEB = 'WEB' + EXCH = 'EXCH' + EXPR = 'EXPR' + EXHTTP = 'EXHTTP' + TYPES = (WEB, EXCH, EXPR, EXHTTP) FIELDS = Fields( - ChoiceField('type', field_uri='Type', choices={ - Choice('WEB'), Choice('EXCH'), Choice('EXPR'), Choice('EXHTTP') - }, namespace=RNS), + ChoiceField('type', field_uri='Type', choices={Choice(c) for c in TYPES}, namespace=RNS), TextField('as_url', field_uri='ASUrl', namespace=RNS), ) __slots__ = tuple(f.name for f in FIELDS)
    @@ -1803,16 +1824,40 @@

    Ancestors

  • AutodiscoverBase
  • EWSElement
  • +

    Subclasses

    +

    Class variables

    var ELEMENT_NAME
    +
    var EXCH
    +
    +
    +
    +
    var EXHTTP
    +
    +
    +
    +
    var EXPR
    +
    +
    +
    var FIELDS
    +
    var TYPES
    +
    +
    +
    +
    var WEB
    +
    +
    +

    Instance variables

    @@ -2037,11 +2082,8 @@

    Protocol

      -
    • ELEMENT_NAME
    • FIELDS
    • -
    • TYPES
    • address_book
    • -
    • as_url
    • auth_package
    • auth_required
    • auth_type
    • @@ -2084,7 +2126,6 @@

      spa
    • ssl
    • ttl
    • -
    • type
    • um_url
    • use_pop_path
    • version
    • @@ -2097,7 +2138,7 @@

      FIELDS
    • account
    • autodiscover_smtp_address
    • -
    • protocol
    • +
    • ews_url
    • redirect_address
    • redirect_url
    • user
    • @@ -2105,9 +2146,14 @@

      SimpleProtocol

      -
        + @@ -2129,7 +2175,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/autodiscover/protocol.html b/docs/exchangelib/autodiscover/protocol.html index cd56a93d..4dbebb6a 100644 --- a/docs/exchangelib/autodiscover/protocol.html +++ b/docs/exchangelib/autodiscover/protocol.html @@ -3,16 +3,16 @@ - + exchangelib.autodiscover.protocol API documentation - + - + @@ -123,7 +123,7 @@

        \ No newline at end of file diff --git a/docs/exchangelib/configuration.html b/docs/exchangelib/configuration.html index 3899c6cc..816df556 100644 --- a/docs/exchangelib/configuration.html +++ b/docs/exchangelib/configuration.html @@ -3,16 +3,16 @@ - + exchangelib.configuration API documentation - + - + @@ -276,7 +276,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/credentials.html b/docs/exchangelib/credentials.html index c3c56e5e..2cedef5a 100644 --- a/docs/exchangelib/credentials.html +++ b/docs/exchangelib/credentials.html @@ -3,17 +3,17 @@ - + exchangelib.credentials API documentation - + - + @@ -824,7 +824,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/errors.html b/docs/exchangelib/errors.html index da73cd90..dc419afc 100644 --- a/docs/exchangelib/errors.html +++ b/docs/exchangelib/errors.html @@ -3,16 +3,16 @@ - + exchangelib.errors API documentation - + - + @@ -33,8 +33,6 @@

        Module exchangelib.errors

        """ from urllib.parse import urlparse -import pytz.exceptions - class MultipleObjectsReturned(Exception): pass @@ -136,14 +134,6 @@

        Module exchangelib.errors

        pass -class AmbiguousTimeError(EWSError, pytz.exceptions.AmbiguousTimeError): - pass - - -class NonExistentTimeError(EWSError, pytz.exceptions.NonExistentTimeError): - pass - - class SessionPoolMinSizeReached(EWSError): pass @@ -595,29 +585,6 @@

        Module exchangelib.errors

        Classes

        -
        -class AmbiguousTimeError -(value) -
        -
        -

        Global error type within this module.

        -
        - -Expand source code - -
        class AmbiguousTimeError(EWSError, pytz.exceptions.AmbiguousTimeError):
        -    pass
        -
        -

        Ancestors

        -
          -
        • EWSError
        • -
        • pytz.exceptions.AmbiguousTimeError
        • -
        • pytz.exceptions.InvalidTimeError
        • -
        • pytz.exceptions.Error
        • -
        • builtins.Exception
        • -
        • builtins.BaseException
        • -
        -
        class AutoDiscoverCircularRedirect (value) @@ -750,7 +717,7 @@

        Ancestors

        class DoesNotExist -(...) +(*args, **kwargs)

        Common base class for all non-exit exceptions.

        @@ -794,10 +761,8 @@

        Ancestors

      Subclasses

      -
      -class NonExistentTimeError -(value) -
      -
      -

      Global error type within this module.

      -
      - -Expand source code - -
      class NonExistentTimeError(EWSError, pytz.exceptions.NonExistentTimeError):
      -    pass
      -
      -

      Ancestors

      -
        -
      • EWSError
      • -
      • pytz.exceptions.NonExistentTimeError
      • -
      • pytz.exceptions.InvalidTimeError
      • -
      • pytz.exceptions.Error
      • -
      • builtins.Exception
      • -
      • builtins.BaseException
      • -
      -
      class RateLimitError (value, url, status_code, total_wait) @@ -9590,9 +9532,6 @@

      Index

    • Classes

      • -

        AmbiguousTimeError

        -
      • -
      • AutoDiscoverCircularRedirect

      • @@ -10775,9 +10714,6 @@

        NaiveDateTimeNotAllowed

      • -

        NonExistentTimeError

        -
      • -
      • RateLimitError

      • @@ -10813,7 +10749,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/ewsdatetime.html b/docs/exchangelib/ewsdatetime.html index 0041f92e..56130060 100644 --- a/docs/exchangelib/ewsdatetime.html +++ b/docs/exchangelib/ewsdatetime.html @@ -3,16 +3,16 @@ - + exchangelib.ewsdatetime API documentation - + - + @@ -28,14 +28,17 @@

        Module exchangelib.ewsdatetime

        import datetime
         import logging
        +import warnings
         
         import dateutil.parser
        -import pytz
        -import pytz.exceptions
        +try:
        +    import zoneinfo
        +except ImportError:
        +    from backports import zoneinfo
         import tzlocal
         
        -from .errors import NaiveDateTimeNotAllowed, UnknownTimeZone, AmbiguousTimeError, NonExistentTimeError
        -from .winzone import PYTZ_TO_MS_TIMEZONE_MAP, MS_TIMEZONE_TO_PYTZ_MAP
        +from .errors import NaiveDateTimeNotAllowed, UnknownTimeZone
        +from .winzone import IANA_TO_MS_TIMEZONE_MAP, MS_TIMEZONE_TO_IANA_MAP
         
         log = logging.getLogger(__name__)
         
        @@ -107,13 +110,6 @@ 

        Module exchangelib.ewsdatetime

        def __new__(cls, *args, **kwargs): # pylint: disable=arguments-differ - # Not all Python versions have the same signature for datetime.datetime - """ - Inherits datetime and adds extra formatting required by EWS. Do not set tzinfo directly. Use - EWSTimeZone.localize() instead. - """ - # We can't use the exact signature of datetime.datetime because we get pickle errors, and implementing pickle - # support requires copy-pasting lots of code from datetime.datetime. if not isinstance(kwargs.get('tzinfo'), (EWSTimeZone, type(None))): raise ValueError('tzinfo must be an EWSTimeZone instance') return super().__new__(cls, *args, **kwargs) @@ -125,8 +121,8 @@

        Module exchangelib.ewsdatetime

        """ if not self.tzinfo: - raise ValueError('EWSDateTime must be timezone-aware') - if self.tzinfo.zone == 'UTC': + raise ValueError('%r must be timezone-aware' % self) + if self.tzinfo.key == 'UTC': return self.strftime('%Y-%m-%dT%H:%M:%SZ') return self.replace(microsecond=0).isoformat() @@ -139,11 +135,13 @@

        Module exchangelib.ewsdatetime

        elif isinstance(d.tzinfo, EWSTimeZone): tz = d.tzinfo else: - tz = EWSTimeZone.from_pytz(d.tzinfo) + tz = EWSTimeZone(d.tzinfo.key) return cls(d.year, d.month, d.day, d.hour, d.minute, d.second, d.microsecond, tzinfo=tz) def astimezone(self, tz=None): - t = super().astimezone(tz=tz) + if tz is None: + tz = EWSTimeZone.localzone() + t = super().astimezone(tz=tz).replace(tzinfo=tz) if isinstance(t, self.__class__): return t return self.from_datetime(t) # We want to return EWSDateTime objects @@ -173,8 +171,7 @@

        Module exchangelib.ewsdatetime

        # Parses several common datetime formats and returns timezone-aware EWSDateTime objects if date_string.endswith('Z'): # UTC datetime - naive_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ') - return UTC.localize(naive_dt) + return super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC) if len(date_string) == 19: # This is probably a naive datetime. Don't allow this, but signal caller with an appropriate error local_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%S') @@ -182,8 +179,10 @@

        Module exchangelib.ewsdatetime

        # This is probably a datetime value with timezone information. This comes in the form '+/-HH:MM' but the Python # strptime '%z' directive cannot yet handle full ISO8601 formatted timezone information (see # http://bugs.python.org/issue15873). Use the 'dateutil' package instead. - aware_dt = dateutil.parser.parse(date_string) - return cls.from_datetime(aware_dt.astimezone(UTC)) # We want to return EWSDateTime objects + aware_dt = dateutil.parser.parse(date_string).astimezone(UTC).replace(tzinfo=UTC) + if isinstance(aware_dt, cls): + return aware_dt + return cls.from_datetime(aware_dt) @classmethod def fromtimestamp(cls, t, tz=None): @@ -220,109 +219,95 @@

        Module exchangelib.ewsdatetime

        return EWSDate.from_date(d) # We want to return EWSDate objects -class EWSTimeZone: +class EWSTimeZone(zoneinfo.ZoneInfo): """Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by services.GetServerTimeZones. """ - PYTZ_TO_MS_MAP = PYTZ_TO_MS_TIMEZONE_MAP - MS_TO_PYTZ_MAP = MS_TIMEZONE_TO_PYTZ_MAP + IANA_TO_MS_MAP = IANA_TO_MS_TIMEZONE_MAP + MS_TO_IANA_MAP = MS_TIMEZONE_TO_IANA_MAP + + def __new__(cls, *args, **kwargs): + try: + instance = super().__new__(cls, *args, **kwargs) + except zoneinfo.ZoneInfoNotFoundError as e: + raise UnknownTimeZone(e.args[0]) + try: + instance.ms_id = cls.IANA_TO_MS_MAP[instance.key][0] + except KeyError: + raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % instance.key) + + # We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but + # EWS happily accepts empty strings. For a full list of timezones supported by the target server, including + # long-format names, see output of services.GetServerTimeZones(account.protocol).call() + instance.ms_name = '' + return instance def __eq__(self, other): - # Microsoft timezones are less granular than pytz, so an EWSTimeZone created from 'Europe/Copenhagen' may return + # Microsoft timezones are less granular than IANA, so an EWSTimeZone created from 'Europe/Copenhagen' may return # from the server as 'Europe/Copenhagen'. We're catering for Microsoft here, so base equality on the Microsoft # timezone ID. - if not hasattr(other, 'ms_id'): - # Due to the type magic in from_pytz(), we cannot use isinstance() here + if not isinstance(other, self.__class__): return NotImplemented return self.ms_id == other.ms_id - def __hash__(self): - # We're shuffling around with base classes in from_pytz(). Make sure we have __hash__() implementation. - return super().__hash__() - @classmethod def from_ms_id(cls, ms_id): # Create a timezone instance from a Microsoft timezone ID. This is lossy because there is not a 1:1 translation - # from MS timezone ID to pytz timezone. + # from MS timezone ID to IANA timezone. try: - return cls.timezone(cls.MS_TO_PYTZ_MAP[ms_id]) + return cls(cls.MS_TO_IANA_MAP[ms_id]) except KeyError: if '/' in ms_id: # EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the # string unaltered. - return cls.timezone(ms_id) + return cls(ms_id) raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id) @classmethod def from_pytz(cls, tz): - # pytz timezones are dynamically generated. Subclass the tz.__class__ and add the extra Microsoft timezone - # labels we need. - - # type() does not allow duplicate base classes. For static timezones, 'cls' and 'tz' are the same class. - base_classes = (cls,) if cls == tz.__class__ else (cls, tz.__class__) - self_cls = type(cls.__name__, base_classes, dict(tz.__class__.__dict__)) - try: - self_cls.ms_id = cls.PYTZ_TO_MS_MAP[tz.zone][0] - except KeyError: - raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % tz.zone) - - # We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but - # EWS happily accepts empty strings. For a full list of timezones supported by the target server, including - # long-format names, see output of services.GetServerTimeZones(account.protocol).call() - self_cls.ms_name = '' + return cls(tz.zone) - self = self_cls() - for k, v in tz.__dict__.items(): - setattr(self, k, v) - return self + @classmethod + def from_dateutil(cls, tz): + key = '/'.join(tz._filename.split('/')[-2:]) + return cls(key) @classmethod def localzone(cls): try: tz = tzlocal.get_localzone() - except pytz.exceptions.UnknownTimeZoneError: + except zoneinfo.ZoneInfoNotFoundError: + # Older versions of tzlocal will raise a pytz exception. Let's not depend on pytz just for that. raise UnknownTimeZone("Failed to guess local timezone") - return cls.from_pytz(tz) + # Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively + return cls(tz.key if hasattr(tz, 'key') else tz.zone) @classmethod def timezone(cls, location): - # Like pytz.timezone() but returning EWSTimeZone instances - try: - tz = pytz.timezone(location) - except pytz.exceptions.UnknownTimeZoneError: - raise UnknownTimeZone("Timezone '%s' is unknown by pytz" % location) - return cls.from_pytz(tz) + warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2) + return cls(location) def normalize(self, dt, is_dst=False): - return self._localize_or_normalize(func='normalize', dt=dt, is_dst=is_dst) + warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2) + return dt def localize(self, dt, is_dst=False): - return self._localize_or_normalize(func='localize', dt=dt, is_dst=is_dst) - - def _localize_or_normalize(self, func, dt, is_dst=False): - """localize() and normalize() have common code paths - - Args: - func: - dt: - is_dst: (Default value = False) - - """ - # super() returns a dt.tzinfo of class pytz.tzinfo.FooBar. We need to return type EWSTimeZone - if is_dst is not False: - # Not all pytz timezones support 'is_dst' argument. Only pass it on if it's set explicitly. - try: - res = getattr(super(EWSTimeZone, self), func)(dt, is_dst=is_dst) - except pytz.exceptions.AmbiguousTimeError as exc: - raise AmbiguousTimeError(str(dt)) from exc - except pytz.exceptions.NonExistentTimeError as exc: - raise NonExistentTimeError(str(dt)) from exc - else: - res = getattr(super(EWSTimeZone, self), func)(dt) - if not isinstance(res.tzinfo, EWSTimeZone): - return res.replace(tzinfo=self.from_pytz(res.tzinfo)) - return res + warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2) + if dt.tzinfo is not None: + raise ValueError('%r must be timezone-unaware' % dt) + dt = dt.replace(tzinfo=self) + if is_dst is not None: + # DST dates are assumed to always be after non-DST dates + dt_before = dt.replace(fold=0) + dt_after = dt.replace(fold=1) + dst_before = dt_before.dst() + dst_after = dt_after.dst() + if dst_before > dst_after: + dt = dt_before if is_dst else dt_after + elif dst_before < dst_after: + dt = dt_after if is_dst else dt_before + return dt def fromutc(self, dt): t = super().fromutc(dt) @@ -331,7 +316,7 @@

        Module exchangelib.ewsdatetime

        return EWSDateTime.from_datetime(t) # We want to return EWSDateTime objects -UTC = EWSTimeZone.timezone('UTC') +UTC = EWSTimeZone('UTC') UTC_NOW = lambda: EWSDateTime.now(tz=UTC) # noqa: E731
        @@ -530,13 +515,6 @@

        Methods

        def __new__(cls, *args, **kwargs): # pylint: disable=arguments-differ - # Not all Python versions have the same signature for datetime.datetime - """ - Inherits datetime and adds extra formatting required by EWS. Do not set tzinfo directly. Use - EWSTimeZone.localize() instead. - """ - # We can't use the exact signature of datetime.datetime because we get pickle errors, and implementing pickle - # support requires copy-pasting lots of code from datetime.datetime. if not isinstance(kwargs.get('tzinfo'), (EWSTimeZone, type(None))): raise ValueError('tzinfo must be an EWSTimeZone instance') return super().__new__(cls, *args, **kwargs) @@ -548,8 +526,8 @@

        Methods

        """ if not self.tzinfo: - raise ValueError('EWSDateTime must be timezone-aware') - if self.tzinfo.zone == 'UTC': + raise ValueError('%r must be timezone-aware' % self) + if self.tzinfo.key == 'UTC': return self.strftime('%Y-%m-%dT%H:%M:%SZ') return self.replace(microsecond=0).isoformat() @@ -562,11 +540,13 @@

        Methods

        elif isinstance(d.tzinfo, EWSTimeZone): tz = d.tzinfo else: - tz = EWSTimeZone.from_pytz(d.tzinfo) + tz = EWSTimeZone(d.tzinfo.key) return cls(d.year, d.month, d.day, d.hour, d.minute, d.second, d.microsecond, tzinfo=tz) def astimezone(self, tz=None): - t = super().astimezone(tz=tz) + if tz is None: + tz = EWSTimeZone.localzone() + t = super().astimezone(tz=tz).replace(tzinfo=tz) if isinstance(t, self.__class__): return t return self.from_datetime(t) # We want to return EWSDateTime objects @@ -596,8 +576,7 @@

        Methods

        # Parses several common datetime formats and returns timezone-aware EWSDateTime objects if date_string.endswith('Z'): # UTC datetime - naive_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ') - return UTC.localize(naive_dt) + return super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC) if len(date_string) == 19: # This is probably a naive datetime. Don't allow this, but signal caller with an appropriate error local_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%S') @@ -605,8 +584,10 @@

        Methods

        # This is probably a datetime value with timezone information. This comes in the form '+/-HH:MM' but the Python # strptime '%z' directive cannot yet handle full ISO8601 formatted timezone information (see # http://bugs.python.org/issue15873). Use the 'dateutil' package instead. - aware_dt = dateutil.parser.parse(date_string) - return cls.from_datetime(aware_dt.astimezone(UTC)) # We want to return EWSDateTime objects + aware_dt = dateutil.parser.parse(date_string).astimezone(UTC).replace(tzinfo=UTC) + if isinstance(aware_dt, cls): + return aware_dt + return cls.from_datetime(aware_dt) @classmethod def fromtimestamp(cls, t, tz=None): @@ -667,7 +648,7 @@

        Static methods

        elif isinstance(d.tzinfo, EWSTimeZone): tz = d.tzinfo else: - tz = EWSTimeZone.from_pytz(d.tzinfo) + tz = EWSTimeZone(d.tzinfo.key) return cls(d.year, d.month, d.day, d.hour, d.minute, d.second, d.microsecond, tzinfo=tz)
        @@ -685,8 +666,7 @@

        Static methods

        # Parses several common datetime formats and returns timezone-aware EWSDateTime objects if date_string.endswith('Z'): # UTC datetime - naive_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ') - return UTC.localize(naive_dt) + return super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC) if len(date_string) == 19: # This is probably a naive datetime. Don't allow this, but signal caller with an appropriate error local_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%S') @@ -694,8 +674,10 @@

        Static methods

        # This is probably a datetime value with timezone information. This comes in the form '+/-HH:MM' but the Python # strptime '%z' directive cannot yet handle full ISO8601 formatted timezone information (see # http://bugs.python.org/issue15873). Use the 'dateutil' package instead. - aware_dt = dateutil.parser.parse(date_string) - return cls.from_datetime(aware_dt.astimezone(UTC)) # We want to return EWSDateTime objects
        + aware_dt = dateutil.parser.parse(date_string).astimezone(UTC).replace(tzinfo=UTC) + if isinstance(aware_dt, cls): + return aware_dt + return cls.from_datetime(aware_dt)
        @@ -782,7 +764,9 @@

        Methods

        Expand source code
        def astimezone(self, tz=None):
        -    t = super().astimezone(tz=tz)
        +    if tz is None:
        +        tz = EWSTimeZone.localzone()
        +    t = super().astimezone(tz=tz).replace(tzinfo=tz)
             if isinstance(t, self.__class__):
                 return t
             return self.from_datetime(t)  # We want to return EWSDateTime objects
        @@ -822,8 +806,8 @@

        Methods

        """ if not self.tzinfo: - raise ValueError('EWSDateTime must be timezone-aware') - if self.tzinfo.zone == 'UTC': + raise ValueError('%r must be timezone-aware' % self) + if self.tzinfo.key == 'UTC': return self.strftime('%Y-%m-%dT%H:%M:%SZ') return self.replace(microsecond=0).isoformat()
        @@ -832,6 +816,7 @@

        Methods

        class EWSTimeZone +(*args, **kwargs)

        Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by @@ -840,109 +825,95 @@

        Methods

        Expand source code -
        class EWSTimeZone:
        +
        class EWSTimeZone(zoneinfo.ZoneInfo):
             """Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by
             services.GetServerTimeZones.
         
             """
        -    PYTZ_TO_MS_MAP = PYTZ_TO_MS_TIMEZONE_MAP
        -    MS_TO_PYTZ_MAP = MS_TIMEZONE_TO_PYTZ_MAP
        +    IANA_TO_MS_MAP = IANA_TO_MS_TIMEZONE_MAP
        +    MS_TO_IANA_MAP = MS_TIMEZONE_TO_IANA_MAP
        +
        +    def __new__(cls, *args, **kwargs):
        +        try:
        +            instance = super().__new__(cls, *args, **kwargs)
        +        except zoneinfo.ZoneInfoNotFoundError as e:
        +            raise UnknownTimeZone(e.args[0])
        +        try:
        +            instance.ms_id = cls.IANA_TO_MS_MAP[instance.key][0]
        +        except KeyError:
        +            raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % instance.key)
        +
        +        # We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but
        +        # EWS happily accepts empty strings. For a full list of timezones supported by the target server, including
        +        # long-format names, see output of services.GetServerTimeZones(account.protocol).call()
        +        instance.ms_name = ''
        +        return instance
         
             def __eq__(self, other):
        -        # Microsoft timezones are less granular than pytz, so an EWSTimeZone created from 'Europe/Copenhagen' may return
        +        # Microsoft timezones are less granular than IANA, so an EWSTimeZone created from 'Europe/Copenhagen' may return
                 # from the server as 'Europe/Copenhagen'. We're catering for Microsoft here, so base equality on the Microsoft
                 # timezone ID.
        -        if not hasattr(other, 'ms_id'):
        -            # Due to the type magic in from_pytz(), we cannot use isinstance() here
        +        if not isinstance(other, self.__class__):
                     return NotImplemented
                 return self.ms_id == other.ms_id
         
        -    def __hash__(self):
        -        # We're shuffling around with base classes in from_pytz(). Make sure we have __hash__() implementation.
        -        return super().__hash__()
        -
             @classmethod
             def from_ms_id(cls, ms_id):
                 # Create a timezone instance from a Microsoft timezone ID. This is lossy because there is not a 1:1 translation
        -        # from MS timezone ID to pytz timezone.
        +        # from MS timezone ID to IANA timezone.
                 try:
        -            return cls.timezone(cls.MS_TO_PYTZ_MAP[ms_id])
        +            return cls(cls.MS_TO_IANA_MAP[ms_id])
                 except KeyError:
                     if '/' in ms_id:
                         # EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
                         # string unaltered.
        -                return cls.timezone(ms_id)
        +                return cls(ms_id)
                     raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)
         
             @classmethod
             def from_pytz(cls, tz):
        -        # pytz timezones are dynamically generated. Subclass the tz.__class__ and add the extra Microsoft timezone
        -        # labels we need.
        -
        -        # type() does not allow duplicate base classes. For static timezones, 'cls' and 'tz' are the same class.
        -        base_classes = (cls,) if cls == tz.__class__ else (cls, tz.__class__)
        -        self_cls = type(cls.__name__, base_classes, dict(tz.__class__.__dict__))
        -        try:
        -            self_cls.ms_id = cls.PYTZ_TO_MS_MAP[tz.zone][0]
        -        except KeyError:
        -            raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % tz.zone)
        -
        -        # We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but
        -        # EWS happily accepts empty strings. For a full list of timezones supported by the target server, including
        -        # long-format names, see output of services.GetServerTimeZones(account.protocol).call()
        -        self_cls.ms_name = ''
        +        return cls(tz.zone)
         
        -        self = self_cls()
        -        for k, v in tz.__dict__.items():
        -            setattr(self, k, v)
        -        return self
        +    @classmethod
        +    def from_dateutil(cls, tz):
        +        key = '/'.join(tz._filename.split('/')[-2:])
        +        return cls(key)
         
             @classmethod
             def localzone(cls):
                 try:
                     tz = tzlocal.get_localzone()
        -        except pytz.exceptions.UnknownTimeZoneError:
        +        except zoneinfo.ZoneInfoNotFoundError:
        +            # Older versions of tzlocal will raise a pytz exception. Let's not depend on pytz just for that.
                     raise UnknownTimeZone("Failed to guess local timezone")
        -        return cls.from_pytz(tz)
        +        # Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively
        +        return cls(tz.key if hasattr(tz, 'key') else tz.zone)
         
             @classmethod
             def timezone(cls, location):
        -        # Like pytz.timezone() but returning EWSTimeZone instances
        -        try:
        -            tz = pytz.timezone(location)
        -        except pytz.exceptions.UnknownTimeZoneError:
        -            raise UnknownTimeZone("Timezone '%s' is unknown by pytz" % location)
        -        return cls.from_pytz(tz)
        +        warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2)
        +        return cls(location)
         
             def normalize(self, dt, is_dst=False):
        -        return self._localize_or_normalize(func='normalize', dt=dt, is_dst=is_dst)
        +        warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2)
        +        return dt
         
             def localize(self, dt, is_dst=False):
        -        return self._localize_or_normalize(func='localize', dt=dt, is_dst=is_dst)
        -
        -    def _localize_or_normalize(self, func, dt, is_dst=False):
        -        """localize() and normalize() have common code paths
        -
        -        Args:
        -          func:
        -          dt:
        -          is_dst:  (Default value = False)
        -
        -        """
        -        # super() returns a dt.tzinfo of class pytz.tzinfo.FooBar. We need to return type EWSTimeZone
        -        if is_dst is not False:
        -            # Not all pytz timezones support 'is_dst' argument. Only pass it on if it's set explicitly.
        -            try:
        -                res = getattr(super(EWSTimeZone, self), func)(dt, is_dst=is_dst)
        -            except pytz.exceptions.AmbiguousTimeError as exc:
        -                raise AmbiguousTimeError(str(dt)) from exc
        -            except pytz.exceptions.NonExistentTimeError as exc:
        -                raise NonExistentTimeError(str(dt)) from exc
        -        else:
        -            res = getattr(super(EWSTimeZone, self), func)(dt)
        -        if not isinstance(res.tzinfo, EWSTimeZone):
        -            return res.replace(tzinfo=self.from_pytz(res.tzinfo))
        -        return res
        +        warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2)
        +        if dt.tzinfo is not None:
        +            raise ValueError('%r must be timezone-unaware' % dt)
        +        dt = dt.replace(tzinfo=self)
        +        if is_dst is not None:
        +            # DST dates are assumed to always be after non-DST dates
        +            dt_before = dt.replace(fold=0)
        +            dt_after = dt.replace(fold=1)
        +            dst_before = dt_before.dst()
        +            dst_after = dt_after.dst()
        +            if dst_before > dst_after:
        +                dt = dt_before if is_dst else dt_after
        +            elif dst_before < dst_after:
        +                dt = dt_after if is_dst else dt_before
        +        return dt
         
             def fromutc(self, dt):
                 t = super().fromutc(dt)
        @@ -950,23 +921,39 @@ 

        Methods

        return t return EWSDateTime.from_datetime(t) # We want to return EWSDateTime objects
        -

        Subclasses

        +

        Ancestors

          -
        • pytz.EWSTimeZone
        • +
        • backports.zoneinfo.ZoneInfo
        • +
        • datetime.tzinfo

        Class variables

        -
        var MS_TO_PYTZ_MAP
        +
        var IANA_TO_MS_MAP
        -
        var PYTZ_TO_MS_MAP
        +
        var MS_TO_IANA_MAP

        Static methods

        +
        +def from_dateutil(tz) +
        +
        +
        +
        + +Expand source code + +
        @classmethod
        +def from_dateutil(cls, tz):
        +    key = '/'.join(tz._filename.split('/')[-2:])
        +    return cls(key)
        +
        +
        def from_ms_id(ms_id)
        @@ -979,14 +966,14 @@

        Static methods

        @classmethod
         def from_ms_id(cls, ms_id):
             # Create a timezone instance from a Microsoft timezone ID. This is lossy because there is not a 1:1 translation
        -    # from MS timezone ID to pytz timezone.
        +    # from MS timezone ID to IANA timezone.
             try:
        -        return cls.timezone(cls.MS_TO_PYTZ_MAP[ms_id])
        +        return cls(cls.MS_TO_IANA_MAP[ms_id])
             except KeyError:
                 if '/' in ms_id:
                     # EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
                     # string unaltered.
        -            return cls.timezone(ms_id)
        +            return cls(ms_id)
                 raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)
        @@ -1001,26 +988,7 @@

        Static methods

        @classmethod
         def from_pytz(cls, tz):
        -    # pytz timezones are dynamically generated. Subclass the tz.__class__ and add the extra Microsoft timezone
        -    # labels we need.
        -
        -    # type() does not allow duplicate base classes. For static timezones, 'cls' and 'tz' are the same class.
        -    base_classes = (cls,) if cls == tz.__class__ else (cls, tz.__class__)
        -    self_cls = type(cls.__name__, base_classes, dict(tz.__class__.__dict__))
        -    try:
        -        self_cls.ms_id = cls.PYTZ_TO_MS_MAP[tz.zone][0]
        -    except KeyError:
        -        raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % tz.zone)
        -
        -    # We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but
        -    # EWS happily accepts empty strings. For a full list of timezones supported by the target server, including
        -    # long-format names, see output of services.GetServerTimeZones(account.protocol).call()
        -    self_cls.ms_name = ''
        -
        -    self = self_cls()
        -    for k, v in tz.__dict__.items():
        -        setattr(self, k, v)
        -    return self
        + return cls(tz.zone)
        @@ -1036,9 +1004,11 @@

        Static methods

        def localzone(cls): try: tz = tzlocal.get_localzone() - except pytz.exceptions.UnknownTimeZoneError: + except zoneinfo.ZoneInfoNotFoundError: + # Older versions of tzlocal will raise a pytz exception. Let's not depend on pytz just for that. raise UnknownTimeZone("Failed to guess local timezone") - return cls.from_pytz(tz)
        + # Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively + return cls(tz.key if hasattr(tz, 'key') else tz.zone)
        @@ -1052,12 +1022,8 @@

        Static methods

        @classmethod
         def timezone(cls, location):
        -    # Like pytz.timezone() but returning EWSTimeZone instances
        -    try:
        -        tz = pytz.timezone(location)
        -    except pytz.exceptions.UnknownTimeZoneError:
        -        raise UnknownTimeZone("Timezone '%s' is unknown by pytz" % location)
        -    return cls.from_pytz(tz)
        + warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2) + return cls(location)

    @@ -1067,7 +1033,7 @@

    Methods

    def fromutc(self, dt)
    -
    +

    Given a datetime with local time in UTC, retrieve an adjusted datetime in local time.

    Expand source code @@ -1089,7 +1055,21 @@

    Methods

    Expand source code
    def localize(self, dt, is_dst=False):
    -    return self._localize_or_normalize(func='localize', dt=dt, is_dst=is_dst)
    + warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2) + if dt.tzinfo is not None: + raise ValueError('%r must be timezone-unaware' % dt) + dt = dt.replace(tzinfo=self) + if is_dst is not None: + # DST dates are assumed to always be after non-DST dates + dt_before = dt.replace(fold=0) + dt_after = dt.replace(fold=1) + dst_before = dt_before.dst() + dst_after = dt_after.dst() + if dst_before > dst_after: + dt = dt_before if is_dst else dt_after + elif dst_before < dst_after: + dt = dt_after if is_dst else dt_before + return dt
    @@ -1102,7 +1082,8 @@

    Methods

    Expand source code
    def normalize(self, dt, is_dst=False):
    -    return self._localize_or_normalize(func='normalize', dt=dt, is_dst=is_dst)
    + warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2) + return dt
    @@ -1154,8 +1135,9 @@

    EWSTimeZone

      -
    • MS_TO_PYTZ_MAP
    • -
    • PYTZ_TO_MS_MAP
    • +
    • IANA_TO_MS_MAP
    • +
    • MS_TO_IANA_MAP
    • +
    • from_dateutil
    • from_ms_id
    • from_pytz
    • fromutc
    • @@ -1171,7 +1153,7 @@

      -

      Generated by pdoc 0.8.4.

      +

      Generated by pdoc 0.9.1.

      \ No newline at end of file diff --git a/docs/exchangelib/extended_properties.html b/docs/exchangelib/extended_properties.html index f0f42317..b09bfcc8 100644 --- a/docs/exchangelib/extended_properties.html +++ b/docs/exchangelib/extended_properties.html @@ -3,16 +3,16 @@ - + exchangelib.extended_properties API documentation - + - + @@ -343,10 +343,10 @@

      Module exchangelib.extended_properties

      class Flag(ExtendedProperty): - """This property returns 0 for Not Flagged messages, 1 for Flagged messages and 2 for Completed messages. + """This property returns None for Not Flagged messages, 1 for Completed messages and 2 for Flagged messages. For a description of each status, see: - https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/flagstatus + https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxoflag/eda9fd25-6407-4cec-9e62-26e4f9d6a098 """ property_tag = 0x1090 @@ -1050,18 +1050,18 @@

      Inherited members

      (*args, **kwargs)
      -

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

      +

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

      For a description of each status, see: -https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/flagstatus

      +https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxoflag/eda9fd25-6407-4cec-9e62-26e4f9d6a098

      Expand source code
      class Flag(ExtendedProperty):
      -    """This property returns 0 for Not Flagged messages, 1 for Flagged messages and 2 for Completed messages.
      +    """This property returns None for Not Flagged messages, 1 for Completed messages and 2 for Flagged messages.
       
           For a description of each status, see:
      -    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/flagstatus
      +    https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxoflag/eda9fd25-6407-4cec-9e62-26e4f9d6a098
       
           """
           property_tag = 0x1090
      @@ -1161,7 +1161,7 @@ 

      -

      Generated by pdoc 0.8.4.

      +

      Generated by pdoc 0.9.1.

      \ No newline at end of file diff --git a/docs/exchangelib/fields.html b/docs/exchangelib/fields.html index 45ae906b..dde82b8e 100644 --- a/docs/exchangelib/fields.html +++ b/docs/exchangelib/fields.html @@ -3,16 +3,16 @@ - + exchangelib.fields API documentation - + - + @@ -32,8 +32,11 @@

      Module exchangelib.fields

      from collections import OrderedDict import datetime from decimal import Decimal, InvalidOperation +from importlib import import_module import logging +import dateutil.parser + from .errors import ErrorInvalidServerVersion from .ewsdatetime import EWSDateTime, EWSDate, EWSTimeZone, NaiveDateTimeNotAllowed, UnknownTimeZone, UTC from .util import create_element, get_xml_attrs, set_xml_value, value_to_xml_text, is_iterable, safe_b64decode, TNS @@ -208,6 +211,13 @@

      Module exchangelib.fields

      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: @@ -570,6 +580,26 @@

      Module exchangelib.fields

      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): value_cls = bytes is_complex = True @@ -620,21 +650,31 @@

      Module exchangelib.fields

      value_cls = datetime.date def __init__(self, *args, **kwargs): + # Not all fields assume a default tim 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 UTC.localize(self._datetime_field.value_cls.combine(value, datetime.time(11, 59))) + 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. 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 manually when we have a + # datetime string with timezone info. + return EWSDate.from_date(dateutil.parser.parse(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. EWS changes all values to have a time of 11:59 local time, so let's send that. + # Convert date to datetime value = self.date_to_datetime(value) return self._datetime_field.to_xml(value=value, version=version) @@ -678,7 +718,7 @@

      Module exchangelib.fields

      # 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', local_dt, self.name, tz) - return tz.localize(local_dt) + return 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', local_dt, self.name) return local_dt @@ -689,9 +729,11 @@

      Module exchangelib.fields

      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. + 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 items, we assume both start and end dates are inclusive. + 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. @@ -709,6 +751,13 @@

      Module exchangelib.fields

      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): value_cls = EWSTimeZone @@ -935,10 +984,19 @@

      Module exchangelib.fields

      class EWSElementField(FieldURIField): def __init__(self, *args, **kwargs): - self.value_cls = kwargs.pop('value_cls') - kwargs['namespace'] = kwargs.get('namespace', self.value_cls.NAMESPACE) + 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()) @@ -990,6 +1048,18 @@

      Module exchangelib.fields

      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 @@ -1526,6 +1596,92 @@

      Returns

      Classes

      +
      +class AppointmentStateField +(*args, **kwargs) +
      +
      +

      Holds information related to an item field

      +
      + +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) @@ -2567,8 +2723,10 @@

      Methods

      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.

      -

      For all-day items, we assume both start and end dates are inclusive.

      +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.

      @@ -2576,9 +2734,11 @@

      Methods

      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.
      +    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 items, we assume both start and end dates are inclusive.
      +    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.
       
      @@ -2594,7 +2754,14 @@ 

      Methods

      # must handle that sanity check. if type(value) == EWSDate: return self._date_field.clean(value=value, version=version) - return super().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

        @@ -2621,6 +2788,23 @@

        Methods

        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

        @@ -2646,21 +2830,31 @@

        Inherited members

        value_cls = datetime.date def __init__(self, *args, **kwargs): + # Not all fields assume a default tim 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 UTC.localize(self._datetime_field.value_cls.combine(value, datetime.time(11, 59))) + 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. 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 manually when we have a + # datetime string with timezone info. + return EWSDate.from_date(dateutil.parser.parse(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. EWS changes all values to have a time of 11:59 local time, so let's send that. + # Convert date to datetime value = self.date_to_datetime(value) return self._datetime_field.to_xml(value=value, version=version) @@ -2688,7 +2882,7 @@

        Methods

        Expand source code
        def date_to_datetime(self, value):
        -    return UTC.localize(self._datetime_field.value_cls.combine(value, datetime.time(11, 59)))
        + return self._datetime_field.value_cls.combine(value, self._default_time).replace(tzinfo=UTC)
        @@ -2701,6 +2895,14 @@

        Methods

        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. 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 manually when we have a
        +        # datetime string with timezone info.
        +        return EWSDate.from_date(dateutil.parser.parse(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
        @@ -2717,7 +2919,7 @@ 

        Methods

        Expand source code
        def to_xml(self, value, version):
        -    # Convert date to datetime. EWS changes all values to have a time of 11:59 local time, so let's send that.
        +    # Convert date to datetime
             value = self.date_to_datetime(value)
             return self._datetime_field.to_xml(value=value, version=version)
        @@ -2755,7 +2957,7 @@

        Methods

        # 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', local_dt, self.name, tz) - return tz.localize(local_dt) + return 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', local_dt, self.name) return local_dt @@ -2818,7 +3020,7 @@

        Methods

        # 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', local_dt, self.name, tz) - return tz.localize(local_dt) + return 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', local_dt, self.name) return local_dt @@ -2869,10 +3071,19 @@

        Inherited members

        class EWSElementField(FieldURIField):
             def __init__(self, *args, **kwargs):
        -        self.value_cls = kwargs.pop('value_cls')
        -        kwargs['namespace'] = kwargs.get('namespace', self.value_cls.NAMESPACE)
        +        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())
        @@ -2911,7 +3122,27 @@ 

        Subclasses

      • PermissionSetField
      • 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

      @@ -3996,6 +4227,13 @@

      Methods

      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: @@ -4101,6 +4339,23 @@

      Methods

      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)
      @@ -4532,6 +4787,7 @@

      Ancestors

    Subclasses

    @@ -4554,7 +4810,7 @@

    Class variables

    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)
    +
    >>> int('0b100', base=0)
     4
     
    @@ -5693,6 +5949,57 @@

    Methods

    +
    +class TaskRecurrenceField +(*args, **kwargs) +
    +
    +

    Holds information related to an item field

    +
    + +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) @@ -6090,6 +6397,17 @@

    Index

  • Classes

    • +

      AppointmentStateField

      + +
    • +
    • AssociatedCalendarItemIdField

    • @@ -6300,9 +6620,10 @@

      FieldPath

      -
        +
        • expand
        • from_string
        • +
        • get_sort_value
        • get_value
        • path
        • to_xml
        • @@ -6471,6 +6792,13 @@

          TaskRecurrenceField

          + + +
        • TextField

          • from_xml
          • @@ -6515,7 +6843,7 @@

            -

            Generated by pdoc 0.8.4.

            +

            Generated by pdoc 0.9.1.

            \ No newline at end of file diff --git a/docs/exchangelib/folders/base.html b/docs/exchangelib/folders/base.html index 226cc18c..c067fdeb 100644 --- a/docs/exchangelib/folders/base.html +++ b/docs/exchangelib/folders/base.html @@ -3,16 +3,16 @@ - + exchangelib.folders.base API documentation - + - + @@ -2690,7 +2690,7 @@

            -

            Generated by pdoc 0.8.4.

            +

            Generated by pdoc 0.9.1.

            \ No newline at end of file diff --git a/docs/exchangelib/folders/collections.html b/docs/exchangelib/folders/collections.html index 5f61bddd..d9fcb134 100644 --- a/docs/exchangelib/folders/collections.html +++ b/docs/exchangelib/folders/collections.html @@ -3,16 +3,16 @@ - + exchangelib.folders.collections API documentation - + - + @@ -1272,7 +1272,7 @@

            -

            Generated by pdoc 0.8.4.

            +

            Generated by pdoc 0.9.1.

            \ No newline at end of file diff --git a/docs/exchangelib/folders/index.html b/docs/exchangelib/folders/index.html index d0cb9d1b..e2164411 100644 --- a/docs/exchangelib/folders/index.html +++ b/docs/exchangelib/folders/index.html @@ -3,16 +3,16 @@ - + exchangelib.folders API documentation - + - + @@ -10030,7 +10030,7 @@

            -

            Generated by pdoc 0.8.4.

            +

            Generated by pdoc 0.9.1.

            \ No newline at end of file diff --git a/docs/exchangelib/folders/known_folders.html b/docs/exchangelib/folders/known_folders.html index ef4ca0fd..c21469ea 100644 --- a/docs/exchangelib/folders/known_folders.html +++ b/docs/exchangelib/folders/known_folders.html @@ -3,16 +3,16 @@ - + exchangelib.folders.known_folders API documentation - + - + @@ -6396,7 +6396,7 @@

            -

            Generated by pdoc 0.8.4.

            +

            Generated by pdoc 0.9.1.

            \ No newline at end of file diff --git a/docs/exchangelib/folders/queryset.html b/docs/exchangelib/folders/queryset.html index 7ac9ad25..d3ca3ae2 100644 --- a/docs/exchangelib/folders/queryset.html +++ b/docs/exchangelib/folders/queryset.html @@ -3,16 +3,16 @@ - + exchangelib.folders.queryset API documentation - + - + @@ -578,7 +578,7 @@

            -

            Generated by pdoc 0.8.4.

            +

            Generated by pdoc 0.9.1.

            \ No newline at end of file diff --git a/docs/exchangelib/folders/roots.html b/docs/exchangelib/folders/roots.html index 22f186c5..49510675 100644 --- a/docs/exchangelib/folders/roots.html +++ b/docs/exchangelib/folders/roots.html @@ -3,16 +3,16 @@ - + exchangelib.folders.roots API documentation - + - + @@ -1379,7 +1379,7 @@

            -

            Generated by pdoc 0.8.4.

            +

            Generated by pdoc 0.9.1.

            \ No newline at end of file diff --git a/docs/exchangelib/index.html b/docs/exchangelib/index.html index 7079d638..e212df6e 100644 --- a/docs/exchangelib/index.html +++ b/docs/exchangelib/index.html @@ -3,16 +3,16 @@ - + exchangelib API documentation - + - + @@ -36,7 +36,7 @@

            Package exchangelib

            from .extended_properties import ExtendedProperty from .folders import Folder, RootOfHierarchy, FolderCollection, SHALLOW, DEEP from .items import AcceptItem, TentativelyAcceptItem, DeclineItem, CalendarItem, CancelCalendarItem, Contact, \ - DistributionList, Message, PostItem, Task + DistributionList, Message, PostItem, Task, ForwardItem, ReplyToItem, ReplyAllToItem from .properties import Body, HTMLBody, ItemId, Mailbox, Attendee, Room, RoomList, UID, DLMailbox from .protocol import FaultTolerance, FailFast, BaseProtocol, NoVerifyHTTPAdapter, TLSClientAuth from .settings import OofSettings @@ -44,7 +44,7 @@

            Package exchangelib

            from .transport import BASIC, DIGEST, NTLM, GSSAPI, SSPI, OAUTH2, CBA from .version import Build, Version -__version__ = '3.2.1' +__version__ = '3.3.0' __all__ = [ '__version__', @@ -57,7 +57,7 @@

            Package exchangelib

            'ExtendedProperty', 'Folder', 'RootOfHierarchy', 'FolderCollection', 'SHALLOW', 'DEEP', 'AcceptItem', 'TentativelyAcceptItem', 'DeclineItem', 'CalendarItem', 'CancelCalendarItem', 'Contact', - 'DistributionList', 'Message', 'PostItem', 'Task', + 'DistributionList', 'Message', 'PostItem', 'Task', 'ForwardItem', 'ReplyToItem', 'ReplyAllToItem', 'ItemId', 'Mailbox', 'DLMailbox', 'Attendee', 'Room', 'RoomList', 'Body', 'HTMLBody', 'UID', 'FailFast', 'FaultTolerance', 'BaseProtocol', 'NoVerifyHTTPAdapter', 'TLSClientAuth', 'OofSettings', @@ -172,7 +172,7 @@

            Sub-modules

            exchangelib.winzone
            -

            A dict to translate from pytz location name to Windows timezone name. Translations taken from +

            A dict to translate from IANA location name to Windows timezone name. Translations taken from …

            @@ -3714,15 +3714,14 @@

            Methods

            AttendeesField('resources', field_uri='calendar:Resources', is_searchable=False), IntegerField('conflicting_meeting_count', field_uri='calendar:ConflictingMeetingCount', is_read_only=True), IntegerField('adjacent_meeting_count', field_uri='calendar:AdjacentMeetingCount', is_read_only=True), - # Placeholder for ConflictingMeetings - # Placeholder for AdjacentMeetings + EWSElementListField('conflicting_meetings', field_uri='calendar:ConflictingMeetings', value_cls='CalendarItem', + namespace=Item.NAMESPACE, is_read_only=True), + EWSElementListField('adjacent_meetings', field_uri='calendar:AdjacentMeetings', value_cls='CalendarItem', + namespace=Item.NAMESPACE, is_read_only=True), CharField('duration', field_uri='calendar:Duration', is_read_only=True), DateTimeField('appointment_reply_time', field_uri='calendar:AppointmentReplyTime', is_read_only=True), IntegerField('appointment_sequence_number', field_uri='calendar:AppointmentSequenceNumber', is_read_only=True), - # Placeholder for AppointmentState - # AppointmentState is an EnumListField-like field, but with bitmask values: - # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/appointmentstate - # We could probably subclass EnumListField to implement this field. + AppointmentStateField('appointment_state', field_uri='calendar:AppointmentState', is_read_only=True), RecurrenceField('recurrence', field_uri='calendar:Recurrence', is_searchable=False), OccurrenceField('first_occurrence', field_uri='calendar:FirstOccurrence', value_cls=FirstOccurrence, is_read_only=True), @@ -3858,16 +3857,23 @@

            Methods

            setattr(item, field_name, val.astimezone(tz).date()) return item + def tz_field_for_field_name(self, field_name): + meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields() + if self.account.version.build < EXCHANGE_2010: + return meeting_tz_field + if field_name == 'start': + return start_tz_field + elif field_name == 'end': + return end_tz_field + raise ValueError('Unsupported field_name') + def date_to_datetime(self, field_name): # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both # start and end values and let EWS apply its logic, but that seems hacky. value = getattr(self, field_name) - if self.account.version.build < EXCHANGE_2010: - tz = self._meeting_timezone - else: - tz = getattr(self, '_%s_timezone' % field_name) - value = tz.localize(EWSDateTime.combine(value, datetime.time(0, 0))) + tz = getattr(self, self.tz_field_for_field_name(field_name).name) + value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz) if field_name == 'end': value += datetime.timedelta(days=1) return value @@ -3971,6 +3977,10 @@

            Instance variables

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

            +
            var adjacent_meetings
            +
            +

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

            +
            var allow_new_time_proposal

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

            @@ -3983,6 +3993,10 @@

            Instance variables

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

            +
            var appointment_state
            +
            +

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

            +
            var conference_type

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

            @@ -3991,6 +4005,10 @@

            Instance variables

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

            +
            var conflicting_meetings
            +
            +

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

            +
            var deleted_occurrences

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

            @@ -4192,11 +4210,8 @@

            Methods

            # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both # start and end values and let EWS apply its logic, but that seems hacky. value = getattr(self, field_name) - if self.account.version.build < EXCHANGE_2010: - tz = self._meeting_timezone - else: - tz = getattr(self, '_%s_timezone' % field_name) - value = tz.localize(EWSDateTime.combine(value, datetime.time(0, 0))) + tz = getattr(self, self.tz_field_for_field_name(field_name).name) + value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz) if field_name == 'end': value += datetime.timedelta(days=1) return value

  • @@ -4294,6 +4309,26 @@

    Returns

    return elem
    +
    +def tz_field_for_field_name(self, field_name) +
    +
    +
    +
    + +Expand source code + +
    def tz_field_for_field_name(self, field_name):
    +    meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
    +    if self.account.version.build < EXCHANGE_2010:
    +        return meeting_tz_field
    +    if field_name == 'start':
    +        return start_tz_field
    +    elif field_name == 'end':
    +        return end_tz_field
    +    raise ValueError('Unsupported field_name')
    +
    +

    Inherited members

      @@ -4337,6 +4372,7 @@

      Inherited members

    • reminder_is_set
    • reminder_minutes_before_start
    • remove_field
    • +
    • response_objects
    • sensitivity
    • size
    • subject
    • @@ -4568,13 +4604,13 @@

      Instance variables

      TextField('initials', field_uri='contacts:Initials'), CharField('middle_name', field_uri='contacts:MiddleName'), TextField('nickname', field_uri='contacts:Nickname'), - # Placeholder for CompleteName + EWSElementField('complete_name', field_uri='contacts:CompleteName', value_cls=CompleteName, is_read_only=True), TextField('company_name', field_uri='contacts:CompanyName'), EmailAddressesField('email_addresses', field_uri='contacts:EmailAddress'), PhysicalAddressField('physical_addresses', field_uri='contacts:PhysicalAddress'), PhoneNumberField('phone_numbers', field_uri='contacts:PhoneNumber'), TextField('assistant_name', field_uri='contacts:AssistantName'), - DateTimeBackedDateField('birthday', field_uri='contacts:Birthday'), + DateTimeBackedDateField('birthday', field_uri='contacts:Birthday', default_time=datetime.time(11, 59)), URIField('business_homepage', field_uri='contacts:BusinessHomePage'), TextListField('children', field_uri='contacts:Children'), TextListField('companies', field_uri='contacts:Companies', is_searchable=False), @@ -4594,7 +4630,8 @@

      Instance variables

      TextField('profession', field_uri='contacts:Profession'), TextField('spouse_name', field_uri='contacts:SpouseName'), CharField('surname', field_uri='contacts:Surname'), - DateTimeBackedDateField('wedding_anniversary', field_uri='contacts:WeddingAnniversary'), + DateTimeBackedDateField('wedding_anniversary', field_uri='contacts:WeddingAnniversary', + default_time=datetime.time(11, 59)), BooleanField('has_picture', field_uri='contacts:HasPicture', supported_from=EXCHANGE_2010, is_read_only=True), TextField('phonetic_full_name', field_uri='contacts:PhoneticFullName', supported_from=EXCHANGE_2013, is_read_only=True), @@ -4610,11 +4647,15 @@

      Instance variables

      # adds photos as FileAttachments on the contact item (with 'is_contact_photo=True'), which automatically flips # the 'has_picture' field. Base64Field('photo', field_uri='contacts:Photo', is_read_only=True), - # Placeholder for UserSMIMECertificate - # Placeholder for MSExchangeCertificate + Base64Field('user_smime_certificate', field_uri='contacts:UserSMIMECertificate', is_read_only=True, + supported_from=EXCHANGE_2010_SP2), + Base64Field('ms_exchange_certificate', field_uri='contacts:MSExchangeCertificate', is_read_only=True, + supported_from=EXCHANGE_2010_SP2), TextField('directory_id', field_uri='contacts:DirectoryId', supported_from=EXCHANGE_2013, is_read_only=True), - # Placeholder for ManagerMailbox - # Placeholder for DirectReports + CharField('manager_mailbox', field_uri='contacts:ManagerMailbox', supported_from=EXCHANGE_2010_SP2, + is_read_only=True), + CharField('direct_reports', field_uri='contacts:DirectReports', supported_from=EXCHANGE_2010_SP2, + is_read_only=True), ) FIELDS = Item.FIELDS + LOCAL_FIELDS @@ -4669,6 +4710,10 @@

      Instance variables

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

      +
      var complete_name
      +
      +

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

      +
      var contact_source

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

      @@ -4677,6 +4722,10 @@

      Instance variables

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

      +
      var direct_reports
      +
      +

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

      +
      var directory_id

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

      @@ -4729,6 +4778,10 @@

      Instance variables

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

      +
      var manager_mailbox
      +
      +

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

      +
      var middle_name

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

      @@ -4737,6 +4790,10 @@

      Instance variables

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

      +
      var ms_exchange_certificate
      +
      +

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

      +
      var nickname

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

      @@ -4789,6 +4846,10 @@

      Instance variables

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

      +
      var user_smime_certificate
      +
      +

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

      +
      var wedding_anniversary

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

      @@ -4836,6 +4897,7 @@

      Inherited members

    • reminder_is_set
    • reminder_minutes_before_start
    • remove_field
    • +
    • response_objects
    • sensitivity
    • size
    • subject
    • @@ -5150,6 +5212,7 @@

      Inherited members

    • reminder_is_set
    • reminder_minutes_before_start
    • remove_field
    • +
    • response_objects
    • sensitivity
    • size
    • subject
    • @@ -5334,13 +5397,6 @@

      Methods

      def __new__(cls, *args, **kwargs): # pylint: disable=arguments-differ - # Not all Python versions have the same signature for datetime.datetime - """ - Inherits datetime and adds extra formatting required by EWS. Do not set tzinfo directly. Use - EWSTimeZone.localize() instead. - """ - # We can't use the exact signature of datetime.datetime because we get pickle errors, and implementing pickle - # support requires copy-pasting lots of code from datetime.datetime. if not isinstance(kwargs.get('tzinfo'), (EWSTimeZone, type(None))): raise ValueError('tzinfo must be an EWSTimeZone instance') return super().__new__(cls, *args, **kwargs) @@ -5352,8 +5408,8 @@

      Methods

      """ if not self.tzinfo: - raise ValueError('EWSDateTime must be timezone-aware') - if self.tzinfo.zone == 'UTC': + raise ValueError('%r must be timezone-aware' % self) + if self.tzinfo.key == 'UTC': return self.strftime('%Y-%m-%dT%H:%M:%SZ') return self.replace(microsecond=0).isoformat() @@ -5366,11 +5422,13 @@

      Methods

      elif isinstance(d.tzinfo, EWSTimeZone): tz = d.tzinfo else: - tz = EWSTimeZone.from_pytz(d.tzinfo) + tz = EWSTimeZone(d.tzinfo.key) return cls(d.year, d.month, d.day, d.hour, d.minute, d.second, d.microsecond, tzinfo=tz) def astimezone(self, tz=None): - t = super().astimezone(tz=tz) + if tz is None: + tz = EWSTimeZone.localzone() + t = super().astimezone(tz=tz).replace(tzinfo=tz) if isinstance(t, self.__class__): return t return self.from_datetime(t) # We want to return EWSDateTime objects @@ -5400,8 +5458,7 @@

      Methods

      # Parses several common datetime formats and returns timezone-aware EWSDateTime objects if date_string.endswith('Z'): # UTC datetime - naive_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ') - return UTC.localize(naive_dt) + return super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC) if len(date_string) == 19: # This is probably a naive datetime. Don't allow this, but signal caller with an appropriate error local_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%S') @@ -5409,8 +5466,10 @@

      Methods

      # This is probably a datetime value with timezone information. This comes in the form '+/-HH:MM' but the Python # strptime '%z' directive cannot yet handle full ISO8601 formatted timezone information (see # http://bugs.python.org/issue15873). Use the 'dateutil' package instead. - aware_dt = dateutil.parser.parse(date_string) - return cls.from_datetime(aware_dt.astimezone(UTC)) # We want to return EWSDateTime objects + aware_dt = dateutil.parser.parse(date_string).astimezone(UTC).replace(tzinfo=UTC) + if isinstance(aware_dt, cls): + return aware_dt + return cls.from_datetime(aware_dt) @classmethod def fromtimestamp(cls, t, tz=None): @@ -5471,7 +5530,7 @@

      Static methods

      elif isinstance(d.tzinfo, EWSTimeZone): tz = d.tzinfo else: - tz = EWSTimeZone.from_pytz(d.tzinfo) + tz = EWSTimeZone(d.tzinfo.key) return cls(d.year, d.month, d.day, d.hour, d.minute, d.second, d.microsecond, tzinfo=tz)
      @@ -5489,8 +5548,7 @@

      Static methods

      # Parses several common datetime formats and returns timezone-aware EWSDateTime objects if date_string.endswith('Z'): # UTC datetime - naive_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ') - return UTC.localize(naive_dt) + return super().strptime(date_string, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC) if len(date_string) == 19: # This is probably a naive datetime. Don't allow this, but signal caller with an appropriate error local_dt = super().strptime(date_string, '%Y-%m-%dT%H:%M:%S') @@ -5498,8 +5556,10 @@

      Static methods

      # This is probably a datetime value with timezone information. This comes in the form '+/-HH:MM' but the Python # strptime '%z' directive cannot yet handle full ISO8601 formatted timezone information (see # http://bugs.python.org/issue15873). Use the 'dateutil' package instead. - aware_dt = dateutil.parser.parse(date_string) - return cls.from_datetime(aware_dt.astimezone(UTC)) # We want to return EWSDateTime objects
      + aware_dt = dateutil.parser.parse(date_string).astimezone(UTC).replace(tzinfo=UTC) + if isinstance(aware_dt, cls): + return aware_dt + return cls.from_datetime(aware_dt)
      @@ -5586,7 +5646,9 @@

      Methods

      Expand source code
      def astimezone(self, tz=None):
      -    t = super().astimezone(tz=tz)
      +    if tz is None:
      +        tz = EWSTimeZone.localzone()
      +    t = super().astimezone(tz=tz).replace(tzinfo=tz)
           if isinstance(t, self.__class__):
               return t
           return self.from_datetime(t)  # We want to return EWSDateTime objects
      @@ -5626,8 +5688,8 @@

      Methods

      """ if not self.tzinfo: - raise ValueError('EWSDateTime must be timezone-aware') - if self.tzinfo.zone == 'UTC': + raise ValueError('%r must be timezone-aware' % self) + if self.tzinfo.key == 'UTC': return self.strftime('%Y-%m-%dT%H:%M:%SZ') return self.replace(microsecond=0).isoformat()
      @@ -5636,6 +5698,7 @@

      Methods

      class EWSTimeZone +(*args, **kwargs)

      Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by @@ -5644,109 +5707,95 @@

      Methods

      Expand source code -
      class EWSTimeZone:
      +
      class EWSTimeZone(zoneinfo.ZoneInfo):
           """Represents a timezone as expected by the EWS TimezoneContext / TimezoneDefinition XML element, and returned by
           services.GetServerTimeZones.
       
           """
      -    PYTZ_TO_MS_MAP = PYTZ_TO_MS_TIMEZONE_MAP
      -    MS_TO_PYTZ_MAP = MS_TIMEZONE_TO_PYTZ_MAP
      +    IANA_TO_MS_MAP = IANA_TO_MS_TIMEZONE_MAP
      +    MS_TO_IANA_MAP = MS_TIMEZONE_TO_IANA_MAP
      +
      +    def __new__(cls, *args, **kwargs):
      +        try:
      +            instance = super().__new__(cls, *args, **kwargs)
      +        except zoneinfo.ZoneInfoNotFoundError as e:
      +            raise UnknownTimeZone(e.args[0])
      +        try:
      +            instance.ms_id = cls.IANA_TO_MS_MAP[instance.key][0]
      +        except KeyError:
      +            raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % instance.key)
      +
      +        # We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but
      +        # EWS happily accepts empty strings. For a full list of timezones supported by the target server, including
      +        # long-format names, see output of services.GetServerTimeZones(account.protocol).call()
      +        instance.ms_name = ''
      +        return instance
       
           def __eq__(self, other):
      -        # Microsoft timezones are less granular than pytz, so an EWSTimeZone created from 'Europe/Copenhagen' may return
      +        # Microsoft timezones are less granular than IANA, so an EWSTimeZone created from 'Europe/Copenhagen' may return
               # from the server as 'Europe/Copenhagen'. We're catering for Microsoft here, so base equality on the Microsoft
               # timezone ID.
      -        if not hasattr(other, 'ms_id'):
      -            # Due to the type magic in from_pytz(), we cannot use isinstance() here
      +        if not isinstance(other, self.__class__):
                   return NotImplemented
               return self.ms_id == other.ms_id
       
      -    def __hash__(self):
      -        # We're shuffling around with base classes in from_pytz(). Make sure we have __hash__() implementation.
      -        return super().__hash__()
      -
           @classmethod
           def from_ms_id(cls, ms_id):
               # Create a timezone instance from a Microsoft timezone ID. This is lossy because there is not a 1:1 translation
      -        # from MS timezone ID to pytz timezone.
      +        # from MS timezone ID to IANA timezone.
               try:
      -            return cls.timezone(cls.MS_TO_PYTZ_MAP[ms_id])
      +            return cls(cls.MS_TO_IANA_MAP[ms_id])
               except KeyError:
                   if '/' in ms_id:
                       # EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
                       # string unaltered.
      -                return cls.timezone(ms_id)
      +                return cls(ms_id)
                   raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)
       
           @classmethod
           def from_pytz(cls, tz):
      -        # pytz timezones are dynamically generated. Subclass the tz.__class__ and add the extra Microsoft timezone
      -        # labels we need.
      +        return cls(tz.zone)
       
      -        # type() does not allow duplicate base classes. For static timezones, 'cls' and 'tz' are the same class.
      -        base_classes = (cls,) if cls == tz.__class__ else (cls, tz.__class__)
      -        self_cls = type(cls.__name__, base_classes, dict(tz.__class__.__dict__))
      -        try:
      -            self_cls.ms_id = cls.PYTZ_TO_MS_MAP[tz.zone][0]
      -        except KeyError:
      -            raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % tz.zone)
      -
      -        # We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but
      -        # EWS happily accepts empty strings. For a full list of timezones supported by the target server, including
      -        # long-format names, see output of services.GetServerTimeZones(account.protocol).call()
      -        self_cls.ms_name = ''
      -
      -        self = self_cls()
      -        for k, v in tz.__dict__.items():
      -            setattr(self, k, v)
      -        return self
      +    @classmethod
      +    def from_dateutil(cls, tz):
      +        key = '/'.join(tz._filename.split('/')[-2:])
      +        return cls(key)
       
           @classmethod
           def localzone(cls):
               try:
                   tz = tzlocal.get_localzone()
      -        except pytz.exceptions.UnknownTimeZoneError:
      +        except zoneinfo.ZoneInfoNotFoundError:
      +            # Older versions of tzlocal will raise a pytz exception. Let's not depend on pytz just for that.
                   raise UnknownTimeZone("Failed to guess local timezone")
      -        return cls.from_pytz(tz)
      +        # Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively
      +        return cls(tz.key if hasattr(tz, 'key') else tz.zone)
       
           @classmethod
           def timezone(cls, location):
      -        # Like pytz.timezone() but returning EWSTimeZone instances
      -        try:
      -            tz = pytz.timezone(location)
      -        except pytz.exceptions.UnknownTimeZoneError:
      -            raise UnknownTimeZone("Timezone '%s' is unknown by pytz" % location)
      -        return cls.from_pytz(tz)
      +        warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2)
      +        return cls(location)
       
           def normalize(self, dt, is_dst=False):
      -        return self._localize_or_normalize(func='normalize', dt=dt, is_dst=is_dst)
      +        warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2)
      +        return dt
       
           def localize(self, dt, is_dst=False):
      -        return self._localize_or_normalize(func='localize', dt=dt, is_dst=is_dst)
      -
      -    def _localize_or_normalize(self, func, dt, is_dst=False):
      -        """localize() and normalize() have common code paths
      -
      -        Args:
      -          func:
      -          dt:
      -          is_dst:  (Default value = False)
      -
      -        """
      -        # super() returns a dt.tzinfo of class pytz.tzinfo.FooBar. We need to return type EWSTimeZone
      -        if is_dst is not False:
      -            # Not all pytz timezones support 'is_dst' argument. Only pass it on if it's set explicitly.
      -            try:
      -                res = getattr(super(EWSTimeZone, self), func)(dt, is_dst=is_dst)
      -            except pytz.exceptions.AmbiguousTimeError as exc:
      -                raise AmbiguousTimeError(str(dt)) from exc
      -            except pytz.exceptions.NonExistentTimeError as exc:
      -                raise NonExistentTimeError(str(dt)) from exc
      -        else:
      -            res = getattr(super(EWSTimeZone, self), func)(dt)
      -        if not isinstance(res.tzinfo, EWSTimeZone):
      -            return res.replace(tzinfo=self.from_pytz(res.tzinfo))
      -        return res
      +        warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2)
      +        if dt.tzinfo is not None:
      +            raise ValueError('%r must be timezone-unaware' % dt)
      +        dt = dt.replace(tzinfo=self)
      +        if is_dst is not None:
      +            # DST dates are assumed to always be after non-DST dates
      +            dt_before = dt.replace(fold=0)
      +            dt_after = dt.replace(fold=1)
      +            dst_before = dt_before.dst()
      +            dst_after = dt_after.dst()
      +            if dst_before > dst_after:
      +                dt = dt_before if is_dst else dt_after
      +            elif dst_before < dst_after:
      +                dt = dt_after if is_dst else dt_before
      +        return dt
       
           def fromutc(self, dt):
               t = super().fromutc(dt)
      @@ -5754,23 +5803,39 @@ 

      Methods

      return t return EWSDateTime.from_datetime(t) # We want to return EWSDateTime objects
      -

      Subclasses

      +

      Ancestors

        -
      • pytz.EWSTimeZone
      • +
      • backports.zoneinfo.ZoneInfo
      • +
      • datetime.tzinfo

      Class variables

      -
      var MS_TO_PYTZ_MAP
      +
      var IANA_TO_MS_MAP
      -
      var PYTZ_TO_MS_MAP
      +
      var MS_TO_IANA_MAP

      Static methods

      +
      +def from_dateutil(tz) +
      +
      +
      +
      + +Expand source code + +
      @classmethod
      +def from_dateutil(cls, tz):
      +    key = '/'.join(tz._filename.split('/')[-2:])
      +    return cls(key)
      +
      +
      def from_ms_id(ms_id)
      @@ -5783,14 +5848,14 @@

      Static methods

      @classmethod
       def from_ms_id(cls, ms_id):
           # Create a timezone instance from a Microsoft timezone ID. This is lossy because there is not a 1:1 translation
      -    # from MS timezone ID to pytz timezone.
      +    # from MS timezone ID to IANA timezone.
           try:
      -        return cls.timezone(cls.MS_TO_PYTZ_MAP[ms_id])
      +        return cls(cls.MS_TO_IANA_MAP[ms_id])
           except KeyError:
               if '/' in ms_id:
                   # EWS sometimes returns an ID that has a region/location format, e.g. 'Europe/Copenhagen'. Try the
                   # string unaltered.
      -            return cls.timezone(ms_id)
      +            return cls(ms_id)
               raise UnknownTimeZone("Windows timezone ID '%s' is unknown by CLDR" % ms_id)
      @@ -5805,26 +5870,7 @@

      Static methods

      @classmethod
       def from_pytz(cls, tz):
      -    # pytz timezones are dynamically generated. Subclass the tz.__class__ and add the extra Microsoft timezone
      -    # labels we need.
      -
      -    # type() does not allow duplicate base classes. For static timezones, 'cls' and 'tz' are the same class.
      -    base_classes = (cls,) if cls == tz.__class__ else (cls, tz.__class__)
      -    self_cls = type(cls.__name__, base_classes, dict(tz.__class__.__dict__))
      -    try:
      -        self_cls.ms_id = cls.PYTZ_TO_MS_MAP[tz.zone][0]
      -    except KeyError:
      -        raise UnknownTimeZone('No Windows timezone name found for timezone "%s"' % tz.zone)
      -
      -    # We don't need the Windows long-format timezone name in long format. It's used in timezone XML elements, but
      -    # EWS happily accepts empty strings. For a full list of timezones supported by the target server, including
      -    # long-format names, see output of services.GetServerTimeZones(account.protocol).call()
      -    self_cls.ms_name = ''
      -
      -    self = self_cls()
      -    for k, v in tz.__dict__.items():
      -        setattr(self, k, v)
      -    return self
      + return cls(tz.zone)
      @@ -5840,9 +5886,11 @@

      Static methods

      def localzone(cls): try: tz = tzlocal.get_localzone() - except pytz.exceptions.UnknownTimeZoneError: + except zoneinfo.ZoneInfoNotFoundError: + # Older versions of tzlocal will raise a pytz exception. Let's not depend on pytz just for that. raise UnknownTimeZone("Failed to guess local timezone") - return cls.from_pytz(tz)
      + # Handle both old and new versions of tzlocal that may return pytz or zoneinfo objects, respectively + return cls(tz.key if hasattr(tz, 'key') else tz.zone)
      @@ -5856,12 +5904,8 @@

      Static methods

      @classmethod
       def timezone(cls, location):
      -    # Like pytz.timezone() but returning EWSTimeZone instances
      -    try:
      -        tz = pytz.timezone(location)
      -    except pytz.exceptions.UnknownTimeZoneError:
      -        raise UnknownTimeZone("Timezone '%s' is unknown by pytz" % location)
      -    return cls.from_pytz(tz)
      + warnings.warn('replace EWSTimeZone.timezone() with just EWSTimeZone()', DeprecationWarning, stacklevel=2) + return cls(location)
      @@ -5871,7 +5915,7 @@

      Methods

      def fromutc(self, dt)
      -
      +

      Given a datetime with local time in UTC, retrieve an adjusted datetime in local time.

      Expand source code @@ -5893,7 +5937,21 @@

      Methods

      Expand source code
      def localize(self, dt, is_dst=False):
      -    return self._localize_or_normalize(func='localize', dt=dt, is_dst=is_dst)
      + warnings.warn('replace tz.localize() with dt.replace(tzinfo=tz)', DeprecationWarning, stacklevel=2) + if dt.tzinfo is not None: + raise ValueError('%r must be timezone-unaware' % dt) + dt = dt.replace(tzinfo=self) + if is_dst is not None: + # DST dates are assumed to always be after non-DST dates + dt_before = dt.replace(fold=0) + dt_after = dt.replace(fold=1) + dst_before = dt_before.dst() + dst_after = dt_after.dst() + if dst_before > dst_after: + dt = dt_before if is_dst else dt_after + elif dst_before < dst_after: + dt = dt_after if is_dst else dt_before + return dt
      @@ -5906,7 +5964,8 @@

      Methods

      Expand source code
      def normalize(self, dt, is_dst=False):
      -    return self._localize_or_normalize(func='normalize', dt=dt, is_dst=is_dst)
      + warnings.warn('normalization is now handled gracefully', DeprecationWarning, stacklevel=2) + return dt
      @@ -8171,6 +8230,60 @@

      Args

      +
      +class ForwardItem +(**kwargs) +
      +
      + +
      + +Expand source code + +
      class ForwardItem(BaseReplyItem):
      +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/forwarditem"""
      +    ELEMENT_NAME = 'ForwardItem'
      +
      +    __slots__ = tuple()
      +
      +

      Ancestors

      + +

      Class variables

      +
      +
      var ELEMENT_NAME
      +
      +
      +
      +
      +

      Inherited members

      + +
      class HTMLBody (...) @@ -8687,7 +8800,8 @@

      Inherited members

      MailboxListField('reply_to', field_uri='message:ReplyTo', is_read_only_after_send=True, is_searchable=False), MailboxField('received_by', field_uri='message:ReceivedBy', is_read_only=True), MailboxField('received_representing', field_uri='message:ReceivedRepresenting', is_read_only=True), - # Placeholder for ReminderMessageData + EWSElementField('reminder_message_data', field_uri='message:ReminderMessageData', + value_cls=ReminderMessageData, supported_from=EXCHANGE_2013_SP1, is_read_only=True), ) FIELDS = Item.FIELDS + LOCAL_FIELDS @@ -8872,6 +8986,10 @@

      Instance variables

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

      +
      var reminder_message_data
      +
      +

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

      +
      var reply_to

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

      @@ -9092,6 +9210,7 @@

      Inherited members

    • reminder_is_set
    • reminder_minutes_before_start
    • remove_field
    • +
    • response_objects
    • sensitivity
    • size
    • subject
    • @@ -9810,6 +9929,7 @@

      Inherited members

    • reminder_is_set
    • reminder_minutes_before_start
    • remove_field
    • +
    • response_objects
    • sensitivity
    • size
    • subject
    • @@ -10731,6 +10851,114 @@

      Methods

      +
      +class ReplyAllToItem +(**kwargs) +
      +
      + +
      + +Expand source code + +
      class ReplyAllToItem(BaseReplyItem):
      +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replyalltoitem"""
      +    ELEMENT_NAME = 'ReplyAllToItem'
      +
      +    __slots__ = tuple()
      +
      +

      Ancestors

      + +

      Class variables

      +
      +
      var ELEMENT_NAME
      +
      +
      +
      +
      +

      Inherited members

      + +
      +
      +class ReplyToItem +(**kwargs) +
      +
      + +
      + +Expand source code + +
      class ReplyToItem(BaseReplyItem):
      +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replytoitem"""
      +    ELEMENT_NAME = 'ReplyToItem'
      +
      +    __slots__ = tuple()
      +
      +

      Ancestors

      + +

      Class variables

      +
      +
      var ELEMENT_NAME
      +
      +
      +
      +
      +

      Inherited members

      + +
      class Room (**kwargs) @@ -11508,7 +11736,7 @@

      Methods

      Choice('NoMatch'), Choice('OwnNew'), Choice('Owned'), Choice('Accepted'), Choice('Declined'), Choice('Max') }, is_read_only=True), CharField('delegator', field_uri='task:Delegator', is_read_only=True), - DateTimeField('due_date', field_uri='task:DueDate'), + DateTimeBackedDateField('due_date', field_uri='task:DueDate'), BooleanField('is_editable', field_uri='task:IsAssignmentEditable', is_read_only=True), BooleanField('is_complete', field_uri='task:IsComplete', is_read_only=True), BooleanField('is_recurring', field_uri='task:IsRecurring', is_read_only=True), @@ -11517,8 +11745,8 @@

      Methods

      CharField('owner', field_uri='task:Owner', is_read_only=True), DecimalField('percent_complete', field_uri='task:PercentComplete', is_required=True, default=Decimal(0.0), min=Decimal(0), max=Decimal(100), is_searchable=False), - # Placeholder for Recurrence - DateTimeField('start_date', field_uri='task:StartDate'), + TaskRecurrenceField('recurrence', field_uri='task:Recurrence', is_searchable=False), + DateTimeBackedDateField('start_date', field_uri='task:StartDate'), ChoiceField('status', field_uri='task:Status', choices={ Choice(NOT_STARTED), Choice('InProgress'), Choice(COMPLETED), Choice('WaitingOnOthers'), Choice('Deferred') }, is_required=True, is_searchable=False, default=NOT_STARTED), @@ -11547,10 +11775,10 @@

      Methods

      # 'complete_date' can be set automatically by the server. Allow some grace between local and server time log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now) self.complete_date = now - if self.start_date and self.complete_date < self.start_date: + if self.start_date and self.complete_date.date() < self.start_date: log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting", self.complete_date, self.start_date) - self.complete_date = self.start_date + self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC) if self.percent_complete is not None: if self.status == self.COMPLETED and self.percent_complete != Decimal(100): # percent_complete must be 100% if task is complete @@ -11671,6 +11899,10 @@

      Instance variables

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

      +
      var recurrence
      +
      +

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

      +
      var start_date

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

      @@ -11717,10 +11949,10 @@

      Methods

      # 'complete_date' can be set automatically by the server. Allow some grace between local and server time log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now) self.complete_date = now - if self.start_date and self.complete_date < self.start_date: + if self.start_date and self.complete_date.date() < self.start_date: log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting", self.complete_date, self.start_date) - self.complete_date = self.start_date + self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC) if self.percent_complete is not None: if self.status == self.COMPLETED and self.percent_complete != Decimal(100): # percent_complete must be 100% if task is complete @@ -11794,6 +12026,7 @@

      Inherited members

    • reminder_is_set
    • reminder_minutes_before_start
    • remove_field
    • +
    • response_objects
    • sensitivity
    • size
    • subject
    • @@ -12340,14 +12573,17 @@

      C
    • FIELDS
    • LOCAL_FIELDS
    • adjacent_meeting_count
    • +
    • adjacent_meetings
    • allow_new_time_proposal
    • appointment_reply_time
    • appointment_sequence_number
    • +
    • appointment_state
    • cancel
    • clean
    • clean_timezone_fields
    • conference_type
    • conflicting_meeting_count
    • +
    • conflicting_meetings
    • date_to_datetime
    • deleted_occurrences
    • duration
    • @@ -12380,6 +12616,7 @@

      C
    • timezone_fields
    • to_xml
    • type
    • +
    • tz_field_for_field_name
    • uid
    • when
    @@ -12410,8 +12647,10 @@

    Contact
  • children
  • companies
  • company_name
  • +
  • complete_name
  • contact_source
  • department
  • +
  • direct_reports
  • directory_id
  • display_name
  • email_addresses
  • @@ -12425,8 +12664,10 @@

    Contact
  • initials
  • job_title
  • manager
  • +
  • manager_mailbox
  • middle_name
  • mileage
  • +
  • ms_exchange_certificate
  • nickname
  • notes
  • office
  • @@ -12440,6 +12681,7 @@

    Contact
  • profession
  • spouse_name
  • surname
  • +
  • user_smime_certificate
  • wedding_anniversary
  • @@ -12501,8 +12743,9 @@

    EWS
  • EWSTimeZone

      -
    • MS_TO_PYTZ_MAP
    • -
    • PYTZ_TO_MS_MAP
    • +
    • IANA_TO_MS_MAP
    • +
    • MS_TO_IANA_MAP
    • +
    • from_dateutil
    • from_ms_id
    • from_pytz
    • fromutc
    • @@ -12606,6 +12849,12 @@

      ForwardItem

      + + +
    • HTMLBody

    • +

      ReplyAllToItem

      + +
    • +
    • +

      ReplyToItem

      + +
    • +
    • Room

      • ELEMENT_NAME
      • @@ -12858,6 +13120,7 @@

        Taskmileage
      • owner
      • percent_complete
      • +
      • recurrence
      • start_date
      • status
      • status_description
      • @@ -12889,7 +13152,7 @@

        Version \ No newline at end of file diff --git a/docs/exchangelib/indexed_properties.html b/docs/exchangelib/indexed_properties.html index f8b14221..8a959021 100644 --- a/docs/exchangelib/indexed_properties.html +++ b/docs/exchangelib/indexed_properties.html @@ -3,16 +3,16 @@ - + exchangelib.indexed_properties API documentation - + - + @@ -567,7 +567,7 @@

        \ No newline at end of file diff --git a/docs/exchangelib/items/base.html b/docs/exchangelib/items/base.html index c531bfc7..89385f43 100644 --- a/docs/exchangelib/items/base.html +++ b/docs/exchangelib/items/base.html @@ -3,16 +3,16 @@ - + exchangelib.items.base API documentation - + - + @@ -927,7 +927,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/items/calendar_item.html b/docs/exchangelib/items/calendar_item.html index a79a9b42..a7e382af 100644 --- a/docs/exchangelib/items/calendar_item.html +++ b/docs/exchangelib/items/calendar_item.html @@ -3,16 +3,16 @@ - + exchangelib.items.calendar_item API documentation - + - + @@ -33,7 +33,7 @@

        Module exchangelib.items.calendar_item

        from ..fields import BooleanField, IntegerField, TextField, ChoiceField, URIField, BodyField, DateTimeField, \ MessageHeaderField, AttachmentField, RecurrenceField, MailboxField, AttendeesField, Choice, OccurrenceField, \ OccurrenceListField, TimeZoneField, CharField, EnumAsIntField, FreeBusyStatusField, ReferenceItemIdField, \ - AssociatedCalendarItemIdField, DateOrDateTimeField + AssociatedCalendarItemIdField, DateOrDateTimeField, EWSElementListField, AppointmentStateField from ..properties import Attendee, ReferenceItemId, AssociatedCalendarItemId, OccurrenceItemId, RecurringMasterItemId, \ Fields from ..recurrence import FirstOccurrence, LastOccurrence, Occurrence, DeletedOccurrence @@ -111,15 +111,14 @@

        Module exchangelib.items.calendar_item

        AttendeesField('resources', field_uri='calendar:Resources', is_searchable=False), IntegerField('conflicting_meeting_count', field_uri='calendar:ConflictingMeetingCount', is_read_only=True), IntegerField('adjacent_meeting_count', field_uri='calendar:AdjacentMeetingCount', is_read_only=True), - # Placeholder for ConflictingMeetings - # Placeholder for AdjacentMeetings + EWSElementListField('conflicting_meetings', field_uri='calendar:ConflictingMeetings', value_cls='CalendarItem', + namespace=Item.NAMESPACE, is_read_only=True), + EWSElementListField('adjacent_meetings', field_uri='calendar:AdjacentMeetings', value_cls='CalendarItem', + namespace=Item.NAMESPACE, is_read_only=True), CharField('duration', field_uri='calendar:Duration', is_read_only=True), DateTimeField('appointment_reply_time', field_uri='calendar:AppointmentReplyTime', is_read_only=True), IntegerField('appointment_sequence_number', field_uri='calendar:AppointmentSequenceNumber', is_read_only=True), - # Placeholder for AppointmentState - # AppointmentState is an EnumListField-like field, but with bitmask values: - # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/appointmentstate - # We could probably subclass EnumListField to implement this field. + AppointmentStateField('appointment_state', field_uri='calendar:AppointmentState', is_read_only=True), RecurrenceField('recurrence', field_uri='calendar:Recurrence', is_searchable=False), OccurrenceField('first_occurrence', field_uri='calendar:FirstOccurrence', value_cls=FirstOccurrence, is_read_only=True), @@ -255,16 +254,23 @@

        Module exchangelib.items.calendar_item

        setattr(item, field_name, val.astimezone(tz).date()) return item + def tz_field_for_field_name(self, field_name): + meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields() + if self.account.version.build < EXCHANGE_2010: + return meeting_tz_field + if field_name == 'start': + return start_tz_field + elif field_name == 'end': + return end_tz_field + raise ValueError('Unsupported field_name') + def date_to_datetime(self, field_name): # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both # start and end values and let EWS apply its logic, but that seems hacky. value = getattr(self, field_name) - if self.account.version.build < EXCHANGE_2010: - tz = self._meeting_timezone - else: - tz = getattr(self, '_%s_timezone' % field_name) - value = tz.localize(EWSDateTime.combine(value, datetime.time(0, 0))) + tz = getattr(self, self.tz_field_for_field_name(field_name).name) + value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz) if field_name == 'end': value += datetime.timedelta(days=1) return value @@ -303,7 +309,7 @@

        Module exchangelib.items.calendar_item

        Therefore BaseMeetingItem inherits from EWSElement has no save() or send() method """ - LOCAL_FIELDS = Message.LOCAL_FIELDS[:-2] + Fields( + LOCAL_FIELDS = Message.LOCAL_FIELDS[:14] + Fields( AssociatedCalendarItemIdField('associated_calendar_item_id', field_uri='meeting:AssociatedCalendarItemId', value_cls=AssociatedCalendarItemId), BooleanField('is_delegated', field_uri='meeting:IsDelegated', is_read_only=True, default=False), @@ -401,7 +407,7 @@

        Module exchangelib.items.calendar_item

        AttachmentField('attachments', field_uri='item:Attachments'), # ItemAttachment or FileAttachment MessageHeaderField('headers', field_uri='item:InternetMessageHeaders', is_read_only=True), ) + Message.LOCAL_FIELDS[:6] + Fields( - ReferenceItemIdField('reference_item_id', field_uri='item:ReferenceItemId', value_cls=ReferenceItemId), + ReferenceItemIdField('reference_item_id', field_uri='item:ReferenceItemId'), MailboxField('received_by', field_uri='message:ReceivedBy', is_read_only=True), MailboxField('received_representing', field_uri='message:ReceivedRepresenting', is_read_only=True), DateTimeField('proposed_start', field_uri='meeting:ProposedStart', supported_from=EXCHANGE_2013), @@ -639,7 +645,7 @@

        Inherited members

        Therefore BaseMeetingItem inherits from EWSElement has no save() or send() method """ - LOCAL_FIELDS = Message.LOCAL_FIELDS[:-2] + Fields( + LOCAL_FIELDS = Message.LOCAL_FIELDS[:14] + Fields( AssociatedCalendarItemIdField('associated_calendar_item_id', field_uri='meeting:AssociatedCalendarItemId', value_cls=AssociatedCalendarItemId), BooleanField('is_delegated', field_uri='meeting:IsDelegated', is_read_only=True, default=False), @@ -801,6 +807,7 @@

        Inherited members

      • reminder_is_set
      • reminder_minutes_before_start
      • remove_field
      • +
      • response_objects
      • sensitivity
      • size
      • subject
      • @@ -835,7 +842,7 @@

        Inherited members

        AttachmentField('attachments', field_uri='item:Attachments'), # ItemAttachment or FileAttachment MessageHeaderField('headers', field_uri='item:InternetMessageHeaders', is_read_only=True), ) + Message.LOCAL_FIELDS[:6] + Fields( - ReferenceItemIdField('reference_item_id', field_uri='item:ReferenceItemId', value_cls=ReferenceItemId), + ReferenceItemIdField('reference_item_id', field_uri='item:ReferenceItemId'), MailboxField('received_by', field_uri='message:ReceivedBy', is_read_only=True), MailboxField('received_representing', field_uri='message:ReceivedRepresenting', is_read_only=True), DateTimeField('proposed_start', field_uri='meeting:ProposedStart', supported_from=EXCHANGE_2013), @@ -1023,15 +1030,14 @@

        Inherited members

        AttendeesField('resources', field_uri='calendar:Resources', is_searchable=False), IntegerField('conflicting_meeting_count', field_uri='calendar:ConflictingMeetingCount', is_read_only=True), IntegerField('adjacent_meeting_count', field_uri='calendar:AdjacentMeetingCount', is_read_only=True), - # Placeholder for ConflictingMeetings - # Placeholder for AdjacentMeetings + EWSElementListField('conflicting_meetings', field_uri='calendar:ConflictingMeetings', value_cls='CalendarItem', + namespace=Item.NAMESPACE, is_read_only=True), + EWSElementListField('adjacent_meetings', field_uri='calendar:AdjacentMeetings', value_cls='CalendarItem', + namespace=Item.NAMESPACE, is_read_only=True), CharField('duration', field_uri='calendar:Duration', is_read_only=True), DateTimeField('appointment_reply_time', field_uri='calendar:AppointmentReplyTime', is_read_only=True), IntegerField('appointment_sequence_number', field_uri='calendar:AppointmentSequenceNumber', is_read_only=True), - # Placeholder for AppointmentState - # AppointmentState is an EnumListField-like field, but with bitmask values: - # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/appointmentstate - # We could probably subclass EnumListField to implement this field. + AppointmentStateField('appointment_state', field_uri='calendar:AppointmentState', is_read_only=True), RecurrenceField('recurrence', field_uri='calendar:Recurrence', is_searchable=False), OccurrenceField('first_occurrence', field_uri='calendar:FirstOccurrence', value_cls=FirstOccurrence, is_read_only=True), @@ -1167,16 +1173,23 @@

        Inherited members

        setattr(item, field_name, val.astimezone(tz).date()) return item + def tz_field_for_field_name(self, field_name): + meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields() + if self.account.version.build < EXCHANGE_2010: + return meeting_tz_field + if field_name == 'start': + return start_tz_field + elif field_name == 'end': + return end_tz_field + raise ValueError('Unsupported field_name') + def date_to_datetime(self, field_name): # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both # start and end values and let EWS apply its logic, but that seems hacky. value = getattr(self, field_name) - if self.account.version.build < EXCHANGE_2010: - tz = self._meeting_timezone - else: - tz = getattr(self, '_%s_timezone' % field_name) - value = tz.localize(EWSDateTime.combine(value, datetime.time(0, 0))) + tz = getattr(self, self.tz_field_for_field_name(field_name).name) + value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz) if field_name == 'end': value += datetime.timedelta(days=1) return value @@ -1280,6 +1293,10 @@

        Instance variables

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

        +
        var adjacent_meetings
        +
        +

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

        +
        var allow_new_time_proposal

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

        @@ -1292,6 +1309,10 @@

        Instance variables

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

        +
        var appointment_state
        +
        +

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

        +
        var conference_type

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

        @@ -1300,6 +1321,10 @@

        Instance variables

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

        +
        var conflicting_meetings
        +
        +

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

        +
        var deleted_occurrences

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

        @@ -1501,11 +1526,8 @@

        Methods

        # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both # start and end values and let EWS apply its logic, but that seems hacky. value = getattr(self, field_name) - if self.account.version.build < EXCHANGE_2010: - tz = self._meeting_timezone - else: - tz = getattr(self, '_%s_timezone' % field_name) - value = tz.localize(EWSDateTime.combine(value, datetime.time(0, 0))) + tz = getattr(self, self.tz_field_for_field_name(field_name).name) + value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz) if field_name == 'end': value += datetime.timedelta(days=1) return value
        @@ -1603,6 +1625,26 @@

        Returns

        return elem
        +
        +def tz_field_for_field_name(self, field_name) +
        +
        +
        +
        + +Expand source code + +
        def tz_field_for_field_name(self, field_name):
        +    meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
        +    if self.account.version.build < EXCHANGE_2010:
        +        return meeting_tz_field
        +    if field_name == 'start':
        +        return start_tz_field
        +    elif field_name == 'end':
        +        return end_tz_field
        +    raise ValueError('Unsupported field_name')
        +
        +

        Inherited members

        @@ -2697,11 +2760,14 @@

        FIELDS
      • LOCAL_FIELDS
      • adjacent_meeting_count
      • +
      • adjacent_meetings
      • allow_new_time_proposal
      • appointment_reply_time
      • appointment_sequence_number
      • +
      • appointment_state
      • conference_type
      • conflicting_meeting_count
      • +
      • conflicting_meetings
      • culture_idx
      • deleted_occurrences
      • duration
      • @@ -2763,7 +2829,7 @@

        \ No newline at end of file diff --git a/docs/exchangelib/items/contact.html b/docs/exchangelib/items/contact.html index 3d042e20..932c2d13 100644 --- a/docs/exchangelib/items/contact.html +++ b/docs/exchangelib/items/contact.html @@ -3,16 +3,16 @@ - + exchangelib.items.contact API documentation - + - + @@ -26,13 +26,14 @@

        Module exchangelib.items.contact

        Expand source code -
        import logging
        +
        import datetime
        +import logging
         
         from ..fields import BooleanField, Base64Field, TextField, ChoiceField, URIField, DateTimeBackedDateField, \
             PhoneNumberField, EmailAddressesField, PhysicalAddressField, Choice, MemberListField, CharField, TextListField, \
        -    EmailAddressField, IdElementField
        -from ..properties import PersonaId, IdChangeKeyMixIn, Fields
        -from ..version import EXCHANGE_2010, EXCHANGE_2013
        +    EmailAddressField, IdElementField, EWSElementField
        +from ..properties import PersonaId, IdChangeKeyMixIn, Fields, CompleteName
        +from ..version import EXCHANGE_2010, EXCHANGE_2010_SP2, EXCHANGE_2013
         from .item import Item
         
         log = logging.getLogger(__name__)
        @@ -56,13 +57,13 @@ 

        Module exchangelib.items.contact

        TextField('initials', field_uri='contacts:Initials'), CharField('middle_name', field_uri='contacts:MiddleName'), TextField('nickname', field_uri='contacts:Nickname'), - # Placeholder for CompleteName + EWSElementField('complete_name', field_uri='contacts:CompleteName', value_cls=CompleteName, is_read_only=True), TextField('company_name', field_uri='contacts:CompanyName'), EmailAddressesField('email_addresses', field_uri='contacts:EmailAddress'), PhysicalAddressField('physical_addresses', field_uri='contacts:PhysicalAddress'), PhoneNumberField('phone_numbers', field_uri='contacts:PhoneNumber'), TextField('assistant_name', field_uri='contacts:AssistantName'), - DateTimeBackedDateField('birthday', field_uri='contacts:Birthday'), + DateTimeBackedDateField('birthday', field_uri='contacts:Birthday', default_time=datetime.time(11, 59)), URIField('business_homepage', field_uri='contacts:BusinessHomePage'), TextListField('children', field_uri='contacts:Children'), TextListField('companies', field_uri='contacts:Companies', is_searchable=False), @@ -82,7 +83,8 @@

        Module exchangelib.items.contact

        TextField('profession', field_uri='contacts:Profession'), TextField('spouse_name', field_uri='contacts:SpouseName'), CharField('surname', field_uri='contacts:Surname'), - DateTimeBackedDateField('wedding_anniversary', field_uri='contacts:WeddingAnniversary'), + DateTimeBackedDateField('wedding_anniversary', field_uri='contacts:WeddingAnniversary', + default_time=datetime.time(11, 59)), BooleanField('has_picture', field_uri='contacts:HasPicture', supported_from=EXCHANGE_2010, is_read_only=True), TextField('phonetic_full_name', field_uri='contacts:PhoneticFullName', supported_from=EXCHANGE_2013, is_read_only=True), @@ -98,11 +100,15 @@

        Module exchangelib.items.contact

        # adds photos as FileAttachments on the contact item (with 'is_contact_photo=True'), which automatically flips # the 'has_picture' field. Base64Field('photo', field_uri='contacts:Photo', is_read_only=True), - # Placeholder for UserSMIMECertificate - # Placeholder for MSExchangeCertificate + Base64Field('user_smime_certificate', field_uri='contacts:UserSMIMECertificate', is_read_only=True, + supported_from=EXCHANGE_2010_SP2), + Base64Field('ms_exchange_certificate', field_uri='contacts:MSExchangeCertificate', is_read_only=True, + supported_from=EXCHANGE_2010_SP2), TextField('directory_id', field_uri='contacts:DirectoryId', supported_from=EXCHANGE_2013, is_read_only=True), - # Placeholder for ManagerMailbox - # Placeholder for DirectReports + CharField('manager_mailbox', field_uri='contacts:ManagerMailbox', supported_from=EXCHANGE_2010_SP2, + is_read_only=True), + CharField('direct_reports', field_uri='contacts:DirectReports', supported_from=EXCHANGE_2010_SP2, + is_read_only=True), ) FIELDS = Item.FIELDS + LOCAL_FIELDS @@ -186,13 +192,13 @@

        Classes

        TextField('initials', field_uri='contacts:Initials'), CharField('middle_name', field_uri='contacts:MiddleName'), TextField('nickname', field_uri='contacts:Nickname'), - # Placeholder for CompleteName + EWSElementField('complete_name', field_uri='contacts:CompleteName', value_cls=CompleteName, is_read_only=True), TextField('company_name', field_uri='contacts:CompanyName'), EmailAddressesField('email_addresses', field_uri='contacts:EmailAddress'), PhysicalAddressField('physical_addresses', field_uri='contacts:PhysicalAddress'), PhoneNumberField('phone_numbers', field_uri='contacts:PhoneNumber'), TextField('assistant_name', field_uri='contacts:AssistantName'), - DateTimeBackedDateField('birthday', field_uri='contacts:Birthday'), + DateTimeBackedDateField('birthday', field_uri='contacts:Birthday', default_time=datetime.time(11, 59)), URIField('business_homepage', field_uri='contacts:BusinessHomePage'), TextListField('children', field_uri='contacts:Children'), TextListField('companies', field_uri='contacts:Companies', is_searchable=False), @@ -212,7 +218,8 @@

        Classes

        TextField('profession', field_uri='contacts:Profession'), TextField('spouse_name', field_uri='contacts:SpouseName'), CharField('surname', field_uri='contacts:Surname'), - DateTimeBackedDateField('wedding_anniversary', field_uri='contacts:WeddingAnniversary'), + DateTimeBackedDateField('wedding_anniversary', field_uri='contacts:WeddingAnniversary', + default_time=datetime.time(11, 59)), BooleanField('has_picture', field_uri='contacts:HasPicture', supported_from=EXCHANGE_2010, is_read_only=True), TextField('phonetic_full_name', field_uri='contacts:PhoneticFullName', supported_from=EXCHANGE_2013, is_read_only=True), @@ -228,11 +235,15 @@

        Classes

        # adds photos as FileAttachments on the contact item (with 'is_contact_photo=True'), which automatically flips # the 'has_picture' field. Base64Field('photo', field_uri='contacts:Photo', is_read_only=True), - # Placeholder for UserSMIMECertificate - # Placeholder for MSExchangeCertificate + Base64Field('user_smime_certificate', field_uri='contacts:UserSMIMECertificate', is_read_only=True, + supported_from=EXCHANGE_2010_SP2), + Base64Field('ms_exchange_certificate', field_uri='contacts:MSExchangeCertificate', is_read_only=True, + supported_from=EXCHANGE_2010_SP2), TextField('directory_id', field_uri='contacts:DirectoryId', supported_from=EXCHANGE_2013, is_read_only=True), - # Placeholder for ManagerMailbox - # Placeholder for DirectReports + CharField('manager_mailbox', field_uri='contacts:ManagerMailbox', supported_from=EXCHANGE_2010_SP2, + is_read_only=True), + CharField('direct_reports', field_uri='contacts:DirectReports', supported_from=EXCHANGE_2010_SP2, + is_read_only=True), ) FIELDS = Item.FIELDS + LOCAL_FIELDS @@ -287,6 +298,10 @@

        Instance variables

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

        +
        var complete_name
        +
        +

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

        +
        var contact_source

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

        @@ -295,6 +310,10 @@

        Instance variables

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

        +
        var direct_reports
        +
        +

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

        +
        var directory_id

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

        @@ -347,6 +366,10 @@

        Instance variables

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

        +
        var manager_mailbox
        +
        +

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

        +
        var middle_name

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

        @@ -355,6 +378,10 @@

        Instance variables

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

        +
        var ms_exchange_certificate
        +
        +

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

        +
        var nickname

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

        @@ -407,6 +434,10 @@

        Instance variables

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

        +
        var user_smime_certificate
        +
        +

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

        +
        var wedding_anniversary

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

        @@ -454,6 +485,7 @@

        Inherited members

      • reminder_is_set
      • reminder_minutes_before_start
      • remove_field
      • +
      • response_objects
      • sensitivity
      • size
      • subject
      • @@ -576,6 +608,7 @@

        Inherited members

      • reminder_is_set
      • reminder_minutes_before_start
      • remove_field
      • +
      • response_objects
      • sensitivity
      • size
      • subject
      • @@ -737,8 +770,10 @@

        children
      • companies
      • company_name
      • +
      • complete_name
      • contact_source
      • department
      • +
      • direct_reports
      • directory_id
      • display_name
      • email_addresses
      • @@ -752,8 +787,10 @@

        initials
      • job_title
      • manager
      • +
      • manager_mailbox
      • middle_name
      • mileage
      • +
      • ms_exchange_certificate
      • nickname
      • notes
      • office
      • @@ -767,6 +804,7 @@

        profession
      • spouse_name
      • surname
      • +
      • user_smime_certificate
      • wedding_anniversary
    • @@ -809,7 +847,7 @@

      -

      Generated by pdoc 0.8.4.

      +

      Generated by pdoc 0.9.1.

      \ No newline at end of file diff --git a/docs/exchangelib/items/index.html b/docs/exchangelib/items/index.html index 5b3eb0fc..ef3f998c 100644 --- a/docs/exchangelib/items/index.html +++ b/docs/exchangelib/items/index.html @@ -3,16 +3,16 @@ - + exchangelib.items API documentation - + - + @@ -424,15 +424,14 @@

      Inherited members

      AttendeesField('resources', field_uri='calendar:Resources', is_searchable=False), IntegerField('conflicting_meeting_count', field_uri='calendar:ConflictingMeetingCount', is_read_only=True), IntegerField('adjacent_meeting_count', field_uri='calendar:AdjacentMeetingCount', is_read_only=True), - # Placeholder for ConflictingMeetings - # Placeholder for AdjacentMeetings + EWSElementListField('conflicting_meetings', field_uri='calendar:ConflictingMeetings', value_cls='CalendarItem', + namespace=Item.NAMESPACE, is_read_only=True), + EWSElementListField('adjacent_meetings', field_uri='calendar:AdjacentMeetings', value_cls='CalendarItem', + namespace=Item.NAMESPACE, is_read_only=True), CharField('duration', field_uri='calendar:Duration', is_read_only=True), DateTimeField('appointment_reply_time', field_uri='calendar:AppointmentReplyTime', is_read_only=True), IntegerField('appointment_sequence_number', field_uri='calendar:AppointmentSequenceNumber', is_read_only=True), - # Placeholder for AppointmentState - # AppointmentState is an EnumListField-like field, but with bitmask values: - # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/appointmentstate - # We could probably subclass EnumListField to implement this field. + AppointmentStateField('appointment_state', field_uri='calendar:AppointmentState', is_read_only=True), RecurrenceField('recurrence', field_uri='calendar:Recurrence', is_searchable=False), OccurrenceField('first_occurrence', field_uri='calendar:FirstOccurrence', value_cls=FirstOccurrence, is_read_only=True), @@ -568,16 +567,23 @@

      Inherited members

      setattr(item, field_name, val.astimezone(tz).date()) return item + def tz_field_for_field_name(self, field_name): + meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields() + if self.account.version.build < EXCHANGE_2010: + return meeting_tz_field + if field_name == 'start': + return start_tz_field + elif field_name == 'end': + return end_tz_field + raise ValueError('Unsupported field_name') + def date_to_datetime(self, field_name): # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both # start and end values and let EWS apply its logic, but that seems hacky. value = getattr(self, field_name) - if self.account.version.build < EXCHANGE_2010: - tz = self._meeting_timezone - else: - tz = getattr(self, '_%s_timezone' % field_name) - value = tz.localize(EWSDateTime.combine(value, datetime.time(0, 0))) + tz = getattr(self, self.tz_field_for_field_name(field_name).name) + value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz) if field_name == 'end': value += datetime.timedelta(days=1) return value @@ -681,6 +687,10 @@

      Instance variables

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

      +
      var adjacent_meetings
      +
      +

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

      +
      var allow_new_time_proposal

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

      @@ -693,6 +703,10 @@

      Instance variables

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

      +
      var appointment_state
      +
      +

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

      +
      var conference_type

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

      @@ -701,6 +715,10 @@

      Instance variables

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

      +
      var conflicting_meetings
      +
      +

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

      +
      var deleted_occurrences

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

      @@ -902,11 +920,8 @@

      Methods

      # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both # start and end values and let EWS apply its logic, but that seems hacky. value = getattr(self, field_name) - if self.account.version.build < EXCHANGE_2010: - tz = self._meeting_timezone - else: - tz = getattr(self, '_%s_timezone' % field_name) - value = tz.localize(EWSDateTime.combine(value, datetime.time(0, 0))) + tz = getattr(self, self.tz_field_for_field_name(field_name).name) + value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz) if field_name == 'end': value += datetime.timedelta(days=1) return value
      @@ -1004,6 +1019,26 @@

      Returns

      return elem
      +
      +def tz_field_for_field_name(self, field_name) +
      +
      +
      +
      + +Expand source code + +
      def tz_field_for_field_name(self, field_name):
      +    meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
      +    if self.account.version.build < EXCHANGE_2010:
      +        return meeting_tz_field
      +    if field_name == 'start':
      +        return start_tz_field
      +    elif field_name == 'end':
      +        return end_tz_field
      +    raise ValueError('Unsupported field_name')
      +
      +

      Inherited members

        @@ -1047,6 +1082,7 @@

        Inherited members

      • reminder_is_set
      • reminder_minutes_before_start
      • remove_field
      • +
      • response_objects
      • sensitivity
      • size
      • subject
      • @@ -1146,13 +1182,13 @@

        Inherited members

        TextField('initials', field_uri='contacts:Initials'), CharField('middle_name', field_uri='contacts:MiddleName'), TextField('nickname', field_uri='contacts:Nickname'), - # Placeholder for CompleteName + EWSElementField('complete_name', field_uri='contacts:CompleteName', value_cls=CompleteName, is_read_only=True), TextField('company_name', field_uri='contacts:CompanyName'), EmailAddressesField('email_addresses', field_uri='contacts:EmailAddress'), PhysicalAddressField('physical_addresses', field_uri='contacts:PhysicalAddress'), PhoneNumberField('phone_numbers', field_uri='contacts:PhoneNumber'), TextField('assistant_name', field_uri='contacts:AssistantName'), - DateTimeBackedDateField('birthday', field_uri='contacts:Birthday'), + DateTimeBackedDateField('birthday', field_uri='contacts:Birthday', default_time=datetime.time(11, 59)), URIField('business_homepage', field_uri='contacts:BusinessHomePage'), TextListField('children', field_uri='contacts:Children'), TextListField('companies', field_uri='contacts:Companies', is_searchable=False), @@ -1172,7 +1208,8 @@

        Inherited members

        TextField('profession', field_uri='contacts:Profession'), TextField('spouse_name', field_uri='contacts:SpouseName'), CharField('surname', field_uri='contacts:Surname'), - DateTimeBackedDateField('wedding_anniversary', field_uri='contacts:WeddingAnniversary'), + DateTimeBackedDateField('wedding_anniversary', field_uri='contacts:WeddingAnniversary', + default_time=datetime.time(11, 59)), BooleanField('has_picture', field_uri='contacts:HasPicture', supported_from=EXCHANGE_2010, is_read_only=True), TextField('phonetic_full_name', field_uri='contacts:PhoneticFullName', supported_from=EXCHANGE_2013, is_read_only=True), @@ -1188,11 +1225,15 @@

        Inherited members

        # adds photos as FileAttachments on the contact item (with 'is_contact_photo=True'), which automatically flips # the 'has_picture' field. Base64Field('photo', field_uri='contacts:Photo', is_read_only=True), - # Placeholder for UserSMIMECertificate - # Placeholder for MSExchangeCertificate + Base64Field('user_smime_certificate', field_uri='contacts:UserSMIMECertificate', is_read_only=True, + supported_from=EXCHANGE_2010_SP2), + Base64Field('ms_exchange_certificate', field_uri='contacts:MSExchangeCertificate', is_read_only=True, + supported_from=EXCHANGE_2010_SP2), TextField('directory_id', field_uri='contacts:DirectoryId', supported_from=EXCHANGE_2013, is_read_only=True), - # Placeholder for ManagerMailbox - # Placeholder for DirectReports + CharField('manager_mailbox', field_uri='contacts:ManagerMailbox', supported_from=EXCHANGE_2010_SP2, + is_read_only=True), + CharField('direct_reports', field_uri='contacts:DirectReports', supported_from=EXCHANGE_2010_SP2, + is_read_only=True), ) FIELDS = Item.FIELDS + LOCAL_FIELDS @@ -1247,6 +1288,10 @@

        Instance variables

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

        +
        var complete_name
        +
        +

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

        +
        var contact_source

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

        @@ -1255,6 +1300,10 @@

        Instance variables

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

        +
        var direct_reports
        +
        +

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

        +
        var directory_id

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

        @@ -1307,6 +1356,10 @@

        Instance variables

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

        +
        var manager_mailbox
        +
        +

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

        +
        var middle_name

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

        @@ -1315,6 +1368,10 @@

        Instance variables

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

        +
        var ms_exchange_certificate
        +
        +

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

        +
        var nickname

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

        @@ -1367,6 +1424,10 @@

        Instance variables

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

        +
        var user_smime_certificate
        +
        +

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

        +
        var wedding_anniversary

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

        @@ -1414,6 +1475,7 @@

        Inherited members

      • reminder_is_set
      • reminder_minutes_before_start
      • remove_field
      • +
      • response_objects
      • sensitivity
      • size
      • subject
      • @@ -1600,6 +1662,7 @@

        Inherited members

      • reminder_is_set
      • reminder_minutes_before_start
      • remove_field
      • +
      • response_objects
      • sensitivity
      • size
      • subject
      • @@ -1708,6 +1771,8 @@

        Inherited members

        MessageHeaderField('headers', field_uri='item:InternetMessageHeaders', is_read_only=True), DateTimeField('datetime_sent', field_uri='item:DateTimeSent', is_read_only=True), DateTimeField('datetime_created', field_uri='item:DateTimeCreated', is_read_only=True), + EWSElementField('response_objects', field_uri='item:ResponseObjects', value_cls=ResponseObjects, + is_read_only=True,), # Placeholder for ResponseObjects DateTimeField('reminder_due_by', field_uri='item:ReminderDueBy', is_required_after_save=True, is_searchable=False), @@ -1753,6 +1818,7 @@

        Inherited members

        self.attachments = [] def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE): + from .task import Task if self.id: item_id, changekey = self._update( update_fieldnames=update_fields, @@ -1760,9 +1826,14 @@

        Inherited members

        conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations ) - if self.id != item_id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)): + if self.id != item_id \ + and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)) \ + and not isinstance(self, Task): # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so # the ID of this item changes. + # + # When we update certain fields on a task, the ID may change. A full description is available at + # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task raise ValueError("'id' mismatch in returned update response") # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact self._id = self.ID_ELEMENT_CLS(item_id, changekey) @@ -2155,6 +2226,10 @@

        Instance variables

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

        +
        var response_objects
        +
        +

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

        +
        var sensitivity

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

        @@ -2441,6 +2516,7 @@

        Args

        Expand source code
        def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE):
        +    from .task import Task
             if self.id:
                 item_id, changekey = self._update(
                     update_fieldnames=update_fields,
        @@ -2448,9 +2524,14 @@ 

        Args

        conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations ) - if self.id != item_id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)): + if self.id != item_id \ + and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)) \ + and not isinstance(self, Task): # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so # the ID of this item changes. + # + # When we update certain fields on a task, the ID may change. A full description is available at + # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task raise ValueError("'id' mismatch in returned update response") # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact self._id = self.ID_ELEMENT_CLS(item_id, changekey) @@ -2602,6 +2683,7 @@

        Inherited members

      • reminder_minutes_before_start
      • remove_field
      • reply_to
      • +
      • response_objects
      • response_type
      • sender
      • sensitivity
      • @@ -2695,6 +2777,10 @@

        Instance variables

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

        +
        var adjacent_meetings
        +
        +

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

        +
        var allow_new_time_proposal

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

        @@ -2707,6 +2793,10 @@

        Instance variables

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

        +
        var appointment_state
        +
        +

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

        +
        var conference_type

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

        @@ -2715,6 +2805,10 @@

        Instance variables

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

        +
        var conflicting_meetings
        +
        +

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

        +
        var deleted_occurrences

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

        @@ -2886,6 +2980,7 @@

        Inherited members

      • reminder_minutes_before_start
      • remove_field
      • reply_to
      • +
      • response_objects
      • response_type
      • sender
      • sensitivity
      • @@ -3051,6 +3146,7 @@

        Inherited members

      • reminder_minutes_before_start
      • remove_field
      • reply_to
      • +
      • response_objects
      • response_type
      • sender
      • sensitivity
      • @@ -3104,7 +3200,8 @@

        Inherited members

        MailboxListField('reply_to', field_uri='message:ReplyTo', is_read_only_after_send=True, is_searchable=False), MailboxField('received_by', field_uri='message:ReceivedBy', is_read_only=True), MailboxField('received_representing', field_uri='message:ReceivedRepresenting', is_read_only=True), - # Placeholder for ReminderMessageData + EWSElementField('reminder_message_data', field_uri='message:ReminderMessageData', + value_cls=ReminderMessageData, supported_from=EXCHANGE_2013_SP1, is_read_only=True), ) FIELDS = Item.FIELDS + LOCAL_FIELDS @@ -3289,6 +3386,10 @@

        Instance variables

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

        +
        var reminder_message_data
        +
        +

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

        +
        var reply_to

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

        @@ -3509,6 +3610,7 @@

        Inherited members

      • reminder_is_set
      • reminder_minutes_before_start
      • remove_field
      • +
      • response_objects
      • sensitivity
      • size
      • subject
      • @@ -3764,6 +3866,7 @@

        Inherited members

      • reminder_is_set
      • reminder_minutes_before_start
      • remove_field
      • +
      • response_objects
      • sensitivity
      • size
      • subject
      • @@ -3898,6 +4001,10 @@

        Instance variables

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

        +
        var reminder_message_data
        +
        +

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

        +
        var reply_to

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

        @@ -3953,6 +4060,7 @@

        Inherited members

      • reminder_is_set
      • reminder_minutes_before_start
      • remove_field
      • +
      • response_objects
      • sensitivity
      • size
      • subject
      • @@ -4265,7 +4373,7 @@

        Inherited members

        Choice('NoMatch'), Choice('OwnNew'), Choice('Owned'), Choice('Accepted'), Choice('Declined'), Choice('Max') }, is_read_only=True), CharField('delegator', field_uri='task:Delegator', is_read_only=True), - DateTimeField('due_date', field_uri='task:DueDate'), + DateTimeBackedDateField('due_date', field_uri='task:DueDate'), BooleanField('is_editable', field_uri='task:IsAssignmentEditable', is_read_only=True), BooleanField('is_complete', field_uri='task:IsComplete', is_read_only=True), BooleanField('is_recurring', field_uri='task:IsRecurring', is_read_only=True), @@ -4274,8 +4382,8 @@

        Inherited members

        CharField('owner', field_uri='task:Owner', is_read_only=True), DecimalField('percent_complete', field_uri='task:PercentComplete', is_required=True, default=Decimal(0.0), min=Decimal(0), max=Decimal(100), is_searchable=False), - # Placeholder for Recurrence - DateTimeField('start_date', field_uri='task:StartDate'), + TaskRecurrenceField('recurrence', field_uri='task:Recurrence', is_searchable=False), + DateTimeBackedDateField('start_date', field_uri='task:StartDate'), ChoiceField('status', field_uri='task:Status', choices={ Choice(NOT_STARTED), Choice('InProgress'), Choice(COMPLETED), Choice('WaitingOnOthers'), Choice('Deferred') }, is_required=True, is_searchable=False, default=NOT_STARTED), @@ -4304,10 +4412,10 @@

        Inherited members

        # 'complete_date' can be set automatically by the server. Allow some grace between local and server time log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now) self.complete_date = now - if self.start_date and self.complete_date < self.start_date: + if self.start_date and self.complete_date.date() < self.start_date: log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting", self.complete_date, self.start_date) - self.complete_date = self.start_date + self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC) if self.percent_complete is not None: if self.status == self.COMPLETED and self.percent_complete != Decimal(100): # percent_complete must be 100% if task is complete @@ -4428,6 +4536,10 @@

        Instance variables

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

        +
        var recurrence
        +
        +

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

        +
        var start_date

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

        @@ -4474,10 +4586,10 @@

        Methods

        # 'complete_date' can be set automatically by the server. Allow some grace between local and server time log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now) self.complete_date = now - if self.start_date and self.complete_date < self.start_date: + if self.start_date and self.complete_date.date() < self.start_date: log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting", self.complete_date, self.start_date) - self.complete_date = self.start_date + self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC) if self.percent_complete is not None: if self.status == self.COMPLETED and self.percent_complete != Decimal(100): # percent_complete must be 100% if task is complete @@ -4551,6 +4663,7 @@

        Inherited members

      • reminder_is_set
      • reminder_minutes_before_start
      • remove_field
      • +
      • response_objects
      • sensitivity
      • size
      • subject
      • @@ -4687,14 +4800,17 @@

        FIELDS
      • LOCAL_FIELDS
      • adjacent_meeting_count
      • +
      • adjacent_meetings
      • allow_new_time_proposal
      • appointment_reply_time
      • appointment_sequence_number
      • +
      • appointment_state
      • cancel
      • clean
      • clean_timezone_fields
      • conference_type
      • conflicting_meeting_count
      • +
      • conflicting_meetings
      • date_to_datetime
      • deleted_occurrences
      • duration
      • @@ -4727,6 +4843,7 @@

        timezone_fields
      • to_xml
      • type
      • +
      • tz_field_for_field_name
      • uid
      • when
      @@ -4750,8 +4867,10 @@

      children
    • companies
    • company_name
    • +
    • complete_name
    • contact_source
    • department
    • +
    • direct_reports
    • directory_id
    • display_name
    • email_addresses
    • @@ -4765,8 +4884,10 @@

      initials
    • job_title
    • manager
    • +
    • manager_mailbox
    • middle_name
    • mileage
    • +
    • ms_exchange_certificate
    • nickname
    • notes
    • office
    • @@ -4780,6 +4901,7 @@

      profession
    • spouse_name
    • surname
    • +
    • user_smime_certificate
    • wedding_anniversary
  • @@ -4853,6 +4975,7 @@

    Item<
  • reminder_due_by
  • reminder_is_set
  • reminder_minutes_before_start
  • +
  • response_objects
  • save
  • sensitivity
  • size
  • @@ -4877,11 +5000,14 @@

    FIELDS
  • LOCAL_FIELDS
  • adjacent_meeting_count
  • +
  • adjacent_meetings
  • allow_new_time_proposal
  • appointment_reply_time
  • appointment_sequence_number
  • +
  • appointment_state
  • conference_type
  • conflicting_meeting_count
  • +
  • conflicting_meetings
  • culture_idx
  • deleted_occurrences
  • duration
  • @@ -4952,6 +5078,7 @@

    received_by
  • received_representing
  • references
  • +
  • reminder_message_data
  • reply
  • reply_all
  • reply_to
  • @@ -5021,6 +5148,7 @@

    received_by
  • received_representing
  • references
  • +
  • reminder_message_data
  • reply_to
  • sender
  • to_recipients
  • @@ -5073,6 +5201,7 @@

    Task<
  • mileage
  • owner
  • percent_complete
  • +
  • recurrence
  • start_date
  • status
  • status_description
  • @@ -5091,7 +5220,7 @@

    -

    Generated by pdoc 0.8.4.

    +

    Generated by pdoc 0.9.1.

    \ No newline at end of file diff --git a/docs/exchangelib/items/item.html b/docs/exchangelib/items/item.html index a63f6fb7..06bba033 100644 --- a/docs/exchangelib/items/item.html +++ b/docs/exchangelib/items/item.html @@ -3,16 +3,16 @@ - + exchangelib.items.item API documentation - + - + @@ -32,7 +32,7 @@

    Module exchangelib.items.item

    DateTimeField, MessageHeaderField, AttachmentField, Choice, EWSElementField, EffectiveRightsField, CultureField, \ CharField, MimeContentField, FieldPath from ..properties import ConversationId, ParentFolderId, ReferenceItemId, OccurrenceItemId, RecurringMasterItemId,\ - Fields + ResponseObjects, Fields from ..services import GetItem, CreateItem, UpdateItem, DeleteItem, MoveItem, CopyItem, ArchiveItem from ..util import is_iterable, require_account, require_id from ..version import EXCHANGE_2010, EXCHANGE_2013 @@ -73,6 +73,8 @@

    Module exchangelib.items.item

    MessageHeaderField('headers', field_uri='item:InternetMessageHeaders', is_read_only=True), DateTimeField('datetime_sent', field_uri='item:DateTimeSent', is_read_only=True), DateTimeField('datetime_created', field_uri='item:DateTimeCreated', is_read_only=True), + EWSElementField('response_objects', field_uri='item:ResponseObjects', value_cls=ResponseObjects, + is_read_only=True,), # Placeholder for ResponseObjects DateTimeField('reminder_due_by', field_uri='item:ReminderDueBy', is_required_after_save=True, is_searchable=False), @@ -118,6 +120,7 @@

    Module exchangelib.items.item

    self.attachments = [] def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE): + from .task import Task if self.id: item_id, changekey = self._update( update_fieldnames=update_fields, @@ -125,9 +128,14 @@

    Module exchangelib.items.item

    conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations ) - if self.id != item_id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)): + if self.id != item_id \ + and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)) \ + and not isinstance(self, Task): # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so # the ID of this item changes. + # + # When we update certain fields on a task, the ID may change. A full description is available at + # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task raise ValueError("'id' mismatch in returned update response") # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact self._id = self.ID_ELEMENT_CLS(item_id, changekey) @@ -416,6 +424,8 @@

    Classes

    MessageHeaderField('headers', field_uri='item:InternetMessageHeaders', is_read_only=True), DateTimeField('datetime_sent', field_uri='item:DateTimeSent', is_read_only=True), DateTimeField('datetime_created', field_uri='item:DateTimeCreated', is_read_only=True), + EWSElementField('response_objects', field_uri='item:ResponseObjects', value_cls=ResponseObjects, + is_read_only=True,), # Placeholder for ResponseObjects DateTimeField('reminder_due_by', field_uri='item:ReminderDueBy', is_required_after_save=True, is_searchable=False), @@ -461,6 +471,7 @@

    Classes

    self.attachments = [] def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE): + from .task import Task if self.id: item_id, changekey = self._update( update_fieldnames=update_fields, @@ -468,9 +479,14 @@

    Classes

    conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations ) - if self.id != item_id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)): + if self.id != item_id \ + and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)) \ + and not isinstance(self, Task): # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so # the ID of this item changes. + # + # When we update certain fields on a task, the ID may change. A full description is available at + # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task raise ValueError("'id' mismatch in returned update response") # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact self._id = self.ID_ELEMENT_CLS(item_id, changekey) @@ -863,6 +879,10 @@

    Instance variables

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

    +
    var response_objects
    +
    +

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

    +
    var sensitivity

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

    @@ -1149,6 +1169,7 @@

    Args

    Expand source code
    def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE):
    +    from .task import Task
         if self.id:
             item_id, changekey = self._update(
                 update_fieldnames=update_fields,
    @@ -1156,9 +1177,14 @@ 

    Args

    conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations ) - if self.id != item_id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)): + if self.id != item_id \ + and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)) \ + and not isinstance(self, Task): # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so # the ID of this item changes. + # + # When we update certain fields on a task, the ID may change. A full description is available at + # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task raise ValueError("'id' mismatch in returned update response") # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact self._id = self.ID_ELEMENT_CLS(item_id, changekey) @@ -1282,6 +1308,7 @@

    reminder_due_by
  • reminder_is_set
  • reminder_minutes_before_start
  • +
  • response_objects
  • save
  • sensitivity
  • size
  • @@ -1299,7 +1326,7 @@

    -

    Generated by pdoc 0.8.4.

    +

    Generated by pdoc 0.9.1.

    \ No newline at end of file diff --git a/docs/exchangelib/items/message.html b/docs/exchangelib/items/message.html index 4618402b..7915a34d 100644 --- a/docs/exchangelib/items/message.html +++ b/docs/exchangelib/items/message.html @@ -3,16 +3,16 @@ - + exchangelib.items.message API documentation - + - + @@ -28,11 +28,11 @@

    Module exchangelib.items.message

    import logging
     
    -from ..fields import BooleanField, Base64Field, TextField, MailboxField, MailboxListField, CharField
    -from ..properties import ReferenceItemId, Fields
    +from ..fields import BooleanField, Base64Field, TextField, MailboxField, MailboxListField, CharField, EWSElementField
    +from ..properties import ReferenceItemId, ReminderMessageData, Fields
     from ..services import SendItem
     from ..util import require_account, require_id
    -from ..version import EXCHANGE_2013
    +from ..version import EXCHANGE_2013, EXCHANGE_2013_SP1
     from .base import BaseReplyItem
     from .item import Item, AUTO_RESOLVE, SEND_TO_NONE, SEND_ONLY, SEND_AND_SAVE_COPY
     
    @@ -66,7 +66,8 @@ 

    Module exchangelib.items.message

    MailboxListField('reply_to', field_uri='message:ReplyTo', is_read_only_after_send=True, is_searchable=False), MailboxField('received_by', field_uri='message:ReceivedBy', is_read_only=True), MailboxField('received_representing', field_uri='message:ReceivedRepresenting', is_read_only=True), - # Placeholder for ReminderMessageData + EWSElementField('reminder_message_data', field_uri='message:ReminderMessageData', + value_cls=ReminderMessageData, supported_from=EXCHANGE_2013_SP1, is_read_only=True), ) FIELDS = Item.FIELDS + LOCAL_FIELDS @@ -296,7 +297,8 @@

    Inherited members

    MailboxListField('reply_to', field_uri='message:ReplyTo', is_read_only_after_send=True, is_searchable=False), MailboxField('received_by', field_uri='message:ReceivedBy', is_read_only=True), MailboxField('received_representing', field_uri='message:ReceivedRepresenting', is_read_only=True), - # Placeholder for ReminderMessageData + EWSElementField('reminder_message_data', field_uri='message:ReminderMessageData', + value_cls=ReminderMessageData, supported_from=EXCHANGE_2013_SP1, is_read_only=True), ) FIELDS = Item.FIELDS + LOCAL_FIELDS @@ -481,6 +483,10 @@

    Instance variables

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

    +
    var reminder_message_data
    +
    +

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

    +
    var reply_to

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

    @@ -701,6 +707,7 @@

    Inherited members

  • reminder_is_set
  • reminder_minutes_before_start
  • remove_field
  • +
  • response_objects
  • sensitivity
  • size
  • subject
  • @@ -865,6 +872,7 @@

    received_by
  • received_representing
  • references
  • +
  • reminder_message_data
  • reply
  • reply_all
  • reply_to
  • @@ -892,7 +900,7 @@

    -

    Generated by pdoc 0.8.4.

    +

    Generated by pdoc 0.9.1.

    \ No newline at end of file diff --git a/docs/exchangelib/items/post.html b/docs/exchangelib/items/post.html index 5014d30a..42e88ab5 100644 --- a/docs/exchangelib/items/post.html +++ b/docs/exchangelib/items/post.html @@ -3,16 +3,16 @@ - + exchangelib.items.post API documentation - + - + @@ -199,6 +199,7 @@

    Inherited members

  • reminder_is_set
  • reminder_minutes_before_start
  • remove_field
  • +
  • response_objects
  • sensitivity
  • size
  • subject
  • @@ -333,6 +334,10 @@

    Instance variables

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

    +
    var reminder_message_data
    +
    +

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

    +
    var reply_to

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

    @@ -388,6 +393,7 @@

    Inherited members

  • reminder_is_set
  • reminder_minutes_before_start
  • remove_field
  • +
  • response_objects
  • sensitivity
  • size
  • subject
  • @@ -456,6 +462,7 @@

    received_by
  • received_representing
  • references
  • +
  • reminder_message_data
  • reply_to
  • sender
  • to_recipients
  • @@ -467,7 +474,7 @@

    -

    Generated by pdoc 0.8.4.

    +

    Generated by pdoc 0.9.1.

    \ No newline at end of file diff --git a/docs/exchangelib/items/task.html b/docs/exchangelib/items/task.html index 0c4908dc..d59cc90e 100644 --- a/docs/exchangelib/items/task.html +++ b/docs/exchangelib/items/task.html @@ -3,16 +3,16 @@ - + exchangelib.items.task API documentation - + - + @@ -26,12 +26,13 @@

    Module exchangelib.items.task

    Expand source code -
    from decimal import Decimal
    +
    import datetime
    +from decimal import Decimal
     import logging
     
    -from ..ewsdatetime import UTC_NOW
    +from ..ewsdatetime import EWSDateTime, UTC, UTC_NOW
     from ..fields import BooleanField, IntegerField, DecimalField, TextField, ChoiceField, DateTimeField, Choice, \
    -    CharField, TextListField
    +    CharField, TextListField, TaskRecurrenceField, DateTimeBackedDateField
     from ..properties import Fields
     from .item import Item
     
    @@ -56,7 +57,7 @@ 

    Module exchangelib.items.task

    Choice('NoMatch'), Choice('OwnNew'), Choice('Owned'), Choice('Accepted'), Choice('Declined'), Choice('Max') }, is_read_only=True), CharField('delegator', field_uri='task:Delegator', is_read_only=True), - DateTimeField('due_date', field_uri='task:DueDate'), + DateTimeBackedDateField('due_date', field_uri='task:DueDate'), BooleanField('is_editable', field_uri='task:IsAssignmentEditable', is_read_only=True), BooleanField('is_complete', field_uri='task:IsComplete', is_read_only=True), BooleanField('is_recurring', field_uri='task:IsRecurring', is_read_only=True), @@ -65,8 +66,8 @@

    Module exchangelib.items.task

    CharField('owner', field_uri='task:Owner', is_read_only=True), DecimalField('percent_complete', field_uri='task:PercentComplete', is_required=True, default=Decimal(0.0), min=Decimal(0), max=Decimal(100), is_searchable=False), - # Placeholder for Recurrence - DateTimeField('start_date', field_uri='task:StartDate'), + TaskRecurrenceField('recurrence', field_uri='task:Recurrence', is_searchable=False), + DateTimeBackedDateField('start_date', field_uri='task:StartDate'), ChoiceField('status', field_uri='task:Status', choices={ Choice(NOT_STARTED), Choice('InProgress'), Choice(COMPLETED), Choice('WaitingOnOthers'), Choice('Deferred') }, is_required=True, is_searchable=False, default=NOT_STARTED), @@ -95,10 +96,10 @@

    Module exchangelib.items.task

    # 'complete_date' can be set automatically by the server. Allow some grace between local and server time log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now) self.complete_date = now - if self.start_date and self.complete_date < self.start_date: + if self.start_date and self.complete_date.date() < self.start_date: log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting", self.complete_date, self.start_date) - self.complete_date = self.start_date + self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC) if self.percent_complete is not None: if self.status == self.COMPLETED and self.percent_complete != Decimal(100): # percent_complete must be 100% if task is complete @@ -156,7 +157,7 @@

    Classes

    Choice('NoMatch'), Choice('OwnNew'), Choice('Owned'), Choice('Accepted'), Choice('Declined'), Choice('Max') }, is_read_only=True), CharField('delegator', field_uri='task:Delegator', is_read_only=True), - DateTimeField('due_date', field_uri='task:DueDate'), + DateTimeBackedDateField('due_date', field_uri='task:DueDate'), BooleanField('is_editable', field_uri='task:IsAssignmentEditable', is_read_only=True), BooleanField('is_complete', field_uri='task:IsComplete', is_read_only=True), BooleanField('is_recurring', field_uri='task:IsRecurring', is_read_only=True), @@ -165,8 +166,8 @@

    Classes

    CharField('owner', field_uri='task:Owner', is_read_only=True), DecimalField('percent_complete', field_uri='task:PercentComplete', is_required=True, default=Decimal(0.0), min=Decimal(0), max=Decimal(100), is_searchable=False), - # Placeholder for Recurrence - DateTimeField('start_date', field_uri='task:StartDate'), + TaskRecurrenceField('recurrence', field_uri='task:Recurrence', is_searchable=False), + DateTimeBackedDateField('start_date', field_uri='task:StartDate'), ChoiceField('status', field_uri='task:Status', choices={ Choice(NOT_STARTED), Choice('InProgress'), Choice(COMPLETED), Choice('WaitingOnOthers'), Choice('Deferred') }, is_required=True, is_searchable=False, default=NOT_STARTED), @@ -195,10 +196,10 @@

    Classes

    # 'complete_date' can be set automatically by the server. Allow some grace between local and server time log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now) self.complete_date = now - if self.start_date and self.complete_date < self.start_date: + if self.start_date and self.complete_date.date() < self.start_date: log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting", self.complete_date, self.start_date) - self.complete_date = self.start_date + self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC) if self.percent_complete is not None: if self.status == self.COMPLETED and self.percent_complete != Decimal(100): # percent_complete must be 100% if task is complete @@ -319,6 +320,10 @@

    Instance variables

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

    +
    var recurrence
    +
    +

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

    +
    var start_date

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

    @@ -365,10 +370,10 @@

    Methods

    # 'complete_date' can be set automatically by the server. Allow some grace between local and server time log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now) self.complete_date = now - if self.start_date and self.complete_date < self.start_date: + if self.start_date and self.complete_date.date() < self.start_date: log.warning("'complete_date' must be greater than 'start_date' (%s vs %s). Resetting", self.complete_date, self.start_date) - self.complete_date = self.start_date + self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC) if self.percent_complete is not None: if self.status == self.COMPLETED and self.percent_complete != Decimal(100): # percent_complete must be 100% if task is complete @@ -442,6 +447,7 @@

    Inherited members

  • reminder_is_set
  • reminder_minutes_before_start
  • remove_field
  • +
  • response_objects
  • sensitivity
  • size
  • subject
  • @@ -498,6 +504,7 @@

    mileage
  • owner
  • percent_complete
  • +
  • recurrence
  • start_date
  • status
  • status_description
  • @@ -510,7 +517,7 @@

    -

    Generated by pdoc 0.8.4.

    +

    Generated by pdoc 0.9.1.

    \ No newline at end of file diff --git a/docs/exchangelib/properties.html b/docs/exchangelib/properties.html index d0fdc6e7..a47e6819 100644 --- a/docs/exchangelib/properties.html +++ b/docs/exchangelib/properties.html @@ -3,16 +3,16 @@ - + exchangelib.properties API documentation - + - + @@ -38,9 +38,9 @@

    Module exchangelib.properties

    from .fields import SubField, TextField, EmailAddressField, ChoiceField, DateTimeField, EWSElementField, MailboxField, \ Choice, BooleanField, IdField, ExtendedPropertyField, IntegerField, TimeField, EnumField, CharField, EmailField, \ EWSElementListField, EnumListField, FreeBusyStatusField, UnknownEntriesField, MessageField, RecipientAddressField, \ - RoutingTypeField, WEEKDAY_NAMES, FieldPath, Field + RoutingTypeField, WEEKDAY_NAMES, FieldPath, Field, AssociatedCalendarItemIdField, ReferenceItemIdField from .util import get_xml_attr, create_element, set_xml_value, value_to_xml_text, MNS, TNS -from .version import Version, EXCHANGE_2013 +from .version import Version, EXCHANGE_2013, Build log = logging.getLogger(__name__) @@ -1391,6 +1391,99 @@

    Module exchangelib.properties

    __slots__ = tuple(f.name for f in FIELDS) +class CompleteName(EWSElement): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/completename""" + ELEMENT_NAME = 'CompleteName' + FIELDS = Fields( + CharField('title', field_uri='Title'), + CharField('first_name', field_uri='FirstName'), + CharField('middle_name', field_uri='MiddleName'), + CharField('last_name', field_uri='LastName'), + CharField('suffix', field_uri='Suffix'), + CharField('initials', field_uri='Initials'), + CharField('full_name', field_uri='FullName'), + CharField('nickname', field_uri='Nickname'), + CharField('yomi_first_name', field_uri='YomiFirstName'), + CharField('yomi_last_name', field_uri='YomiLastName'), + ) + + __slots__ = tuple(f.name for f in FIELDS) + + +class ReminderMessageData(EWSElement): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/remindermessagedata""" + ELEMENT_NAME = 'ReminderMessageData' + FIELDS = Fields( + CharField('reminder_text', field_uri='ReminderText'), + CharField('location', field_uri='Location'), + TimeField('start_time', field_uri='StartTime'), + TimeField('end_time', field_uri='EndTime'), + AssociatedCalendarItemIdField('associated_calendar_item_id', field_uri='AssociatedCalendarItemId', + supported_from=Build(15, 0, 913, 9)), + ) + + __slots__ = tuple(f.name for f in FIELDS) + + +class AcceptSharingInvitation(EWSElement): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptsharinginvitation""" + ELEMENT_NAME = 'AcceptSharingInvitation' + FIELDS = Fields( + ReferenceItemIdField('reference_item_id', field_uri='item:ReferenceItemId'), + ) + + __slots__ = tuple(f.name for f in FIELDS) + + +class SuppressReadReceipt(EWSElement): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suppressreadreceipt""" + ELEMENT_NAME = 'SuppressReadReceipt' + FIELDS = Fields( + ReferenceItemIdField('reference_item_id', field_uri='item:ReferenceItemId'), + ) + + __slots__ = tuple(f.name for f in FIELDS) + + +class RemoveItem(EWSElement): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/removeitem""" + ELEMENT_NAME = 'RemoveItem' + FIELDS = Fields( + ReferenceItemIdField('reference_item_id', field_uri='item:ReferenceItemId'), + ) + + __slots__ = tuple(f.name for f in FIELDS) + + +class ResponseObjects(EWSElement): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responseobjects""" + ELEMENT_NAME = 'ResponseObjects' + FIELDS = Fields( + EWSElementField('accept_item', field_uri='AcceptItem', value_cls='AcceptItem', + namespace=EWSElement.NAMESPACE), + EWSElementField('tentatively_accept_item', field_uri='TentativelyAcceptItem', value_cls='TentativelyAcceptItem', + namespace=EWSElement.NAMESPACE), + EWSElementField('decline_item', field_uri='DeclineItem', value_cls='DeclineItem', + namespace=EWSElement.NAMESPACE), + EWSElementField('reply_to_item', field_uri='ReplyToItem', value_cls='ReplyToItem', + namespace=EWSElement.NAMESPACE), + EWSElementField('forward_item', field_uri='ForwardItem', value_cls='ForwardItem', + namespace=EWSElement.NAMESPACE), + EWSElementField('reply_all_to_item', field_uri='ReplyAllToItem', value_cls='ReplyAllToItem', + namespace=EWSElement.NAMESPACE), + EWSElementField('cancel_calendar_item', field_uri='CancelCalendarItem', value_cls='CancelCalendarItem', + namespace=EWSElement.NAMESPACE), + EWSElementField('remove_item', field_uri='RemoveItem', value_cls=RemoveItem), + EWSElementField('post_reply_item', field_uri='PostReplyItem', value_cls='PostReplyItem', + namespace=EWSElement.NAMESPACE), + EWSElementField('success_read_receipt', field_uri='SuppressReadReceipt', value_cls=SuppressReadReceipt), + EWSElementField('accept_sharing_invitation', field_uri='AcceptSharingInvitation', + value_cls=AcceptSharingInvitation), + ) + + __slots__ = tuple(f.name for f in FIELDS) + + class IdChangeKeyMixIn(EWSElement): """Base class for classes that have a concept of 'id' and 'changekey' values. The values are actually stored on a separate element but we add convenience methods to hide that fact. @@ -1468,6 +1561,59 @@

    Module exchangelib.properties

    Classes

    +
    +class AcceptSharingInvitation +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class AcceptSharingInvitation(EWSElement):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptsharinginvitation"""
    +    ELEMENT_NAME = 'AcceptSharingInvitation'
    +    FIELDS = Fields(
    +        ReferenceItemIdField('reference_item_id', field_uri='item:ReferenceItemId'),
    +    )
    +
    +    __slots__ = tuple(f.name for f in FIELDS)
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    var FIELDS
    +
    +
    +
    +
    +

    Instance variables

    +
    +
    var reference_item_id
    +
    +

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

    +
    +
    +

    Inherited members

    + +
    class AlternateId (**kwargs) @@ -2321,6 +2467,104 @@

    Inherited members

    +
    +class CompleteName +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class CompleteName(EWSElement):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/completename"""
    +    ELEMENT_NAME = 'CompleteName'
    +    FIELDS = Fields(
    +        CharField('title', field_uri='Title'),
    +        CharField('first_name', field_uri='FirstName'),
    +        CharField('middle_name', field_uri='MiddleName'),
    +        CharField('last_name', field_uri='LastName'),
    +        CharField('suffix', field_uri='Suffix'),
    +        CharField('initials', field_uri='Initials'),
    +        CharField('full_name', field_uri='FullName'),
    +        CharField('nickname', field_uri='Nickname'),
    +        CharField('yomi_first_name', field_uri='YomiFirstName'),
    +        CharField('yomi_last_name', field_uri='YomiLastName'),
    +    )
    +
    +    __slots__ = tuple(f.name for f in FIELDS)
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    var FIELDS
    +
    +
    +
    +
    +

    Instance variables

    +
    +
    var first_name
    +
    +

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

    +
    +
    var full_name
    +
    +

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

    +
    +
    var initials
    +
    +

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

    +
    +
    var last_name
    +
    +

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

    +
    +
    var middle_name
    +
    +

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

    +
    +
    var nickname
    +
    +

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

    +
    +
    var suffix
    +
    +

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

    +
    +
    var title
    +
    +

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

    +
    +
    var yomi_first_name
    +
    +

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

    +
    +
    var yomi_last_name
    +
    +

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

    +
    +
    +

    Inherited members

    + +
    class ConversationId (*args, **kwargs) @@ -2949,6 +3193,7 @@

    Subclasses

  • ExtendedProperty
  • IndexedElement
  • BaseReplyItem
  • +
  • AcceptSharingInvitation
  • AlternateId
  • AlternatePublicFolderId
  • AlternatePublicFolderItemId
  • @@ -2958,6 +3203,7 @@

    Subclasses

  • CalendarEventDetails
  • CalendarPermission
  • CalendarView
  • +
  • CompleteName
  • DelegatePermissions
  • DelegateUser
  • EffectiveRights
  • @@ -2978,7 +3224,11 @@

    Subclasses

  • OutOfOffice
  • Permission
  • PermissionSet
  • +
  • ReminderMessageData
  • +
  • RemoveItem
  • +
  • ResponseObjects
  • SearchableMailbox
  • +
  • SuppressReadReceipt
  • TimeWindow
  • TimeZone
  • TimeZoneTransition
  • @@ -4306,7 +4556,7 @@

    Inherited members

    class InvalidField -(...) +(*args, **kwargs)

    Inappropriate argument value (of correct type).

    @@ -4326,7 +4576,7 @@

    Ancestors

    class InvalidFieldForVersion -(...) +(*args, **kwargs)

    Inappropriate argument value (of correct type).

    @@ -5571,6 +5821,245 @@

    Inherited members

    +
    +class ReminderMessageData +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class ReminderMessageData(EWSElement):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/remindermessagedata"""
    +    ELEMENT_NAME = 'ReminderMessageData'
    +    FIELDS = Fields(
    +        CharField('reminder_text', field_uri='ReminderText'),
    +        CharField('location', field_uri='Location'),
    +        TimeField('start_time', field_uri='StartTime'),
    +        TimeField('end_time', field_uri='EndTime'),
    +        AssociatedCalendarItemIdField('associated_calendar_item_id', field_uri='AssociatedCalendarItemId',
    +                                      supported_from=Build(15, 0, 913, 9)),
    +    )
    +
    +    __slots__ = tuple(f.name for f in FIELDS)
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    var FIELDS
    +
    +
    +
    +
    +

    Instance variables

    +
    +
    var associated_calendar_item_id
    +
    +

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

    +
    +
    var end_time
    +
    +

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

    +
    +
    var location
    +
    +

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

    +
    +
    var reminder_text
    +
    +

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

    +
    +
    var start_time
    +
    +

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

    +
    +
    +

    Inherited members

    + +
    +
    +class RemoveItem +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class RemoveItem(EWSElement):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/removeitem"""
    +    ELEMENT_NAME = 'RemoveItem'
    +    FIELDS = Fields(
    +        ReferenceItemIdField('reference_item_id', field_uri='item:ReferenceItemId'),
    +    )
    +
    +    __slots__ = tuple(f.name for f in FIELDS)
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    var FIELDS
    +
    +
    +
    +
    +

    Instance variables

    +
    +
    var reference_item_id
    +
    +

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

    +
    +
    +

    Inherited members

    + +
    +
    +class ResponseObjects +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class ResponseObjects(EWSElement):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responseobjects"""
    +    ELEMENT_NAME = 'ResponseObjects'
    +    FIELDS = Fields(
    +        EWSElementField('accept_item', field_uri='AcceptItem', value_cls='AcceptItem',
    +                        namespace=EWSElement.NAMESPACE),
    +        EWSElementField('tentatively_accept_item', field_uri='TentativelyAcceptItem', value_cls='TentativelyAcceptItem',
    +                        namespace=EWSElement.NAMESPACE),
    +        EWSElementField('decline_item', field_uri='DeclineItem', value_cls='DeclineItem',
    +                        namespace=EWSElement.NAMESPACE),
    +        EWSElementField('reply_to_item', field_uri='ReplyToItem', value_cls='ReplyToItem',
    +                        namespace=EWSElement.NAMESPACE),
    +        EWSElementField('forward_item', field_uri='ForwardItem', value_cls='ForwardItem',
    +                        namespace=EWSElement.NAMESPACE),
    +        EWSElementField('reply_all_to_item', field_uri='ReplyAllToItem', value_cls='ReplyAllToItem',
    +                        namespace=EWSElement.NAMESPACE),
    +        EWSElementField('cancel_calendar_item', field_uri='CancelCalendarItem', value_cls='CancelCalendarItem',
    +                        namespace=EWSElement.NAMESPACE),
    +        EWSElementField('remove_item', field_uri='RemoveItem', value_cls=RemoveItem),
    +        EWSElementField('post_reply_item', field_uri='PostReplyItem', value_cls='PostReplyItem',
    +                        namespace=EWSElement.NAMESPACE),
    +        EWSElementField('success_read_receipt', field_uri='SuppressReadReceipt', value_cls=SuppressReadReceipt),
    +        EWSElementField('accept_sharing_invitation', field_uri='AcceptSharingInvitation',
    +                        value_cls=AcceptSharingInvitation),
    +    )
    +
    +    __slots__ = tuple(f.name for f in FIELDS)
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    var FIELDS
    +
    +
    +
    +
    +

    Instance variables

    +
    +
    var accept_item
    +
    +

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

    +
    +
    var accept_sharing_invitation
    +
    +

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

    +
    +
    var cancel_calendar_item
    +
    +

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

    +
    +
    var decline_item
    +
    +

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

    +
    +
    var forward_item
    +
    +

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

    +
    +
    var post_reply_item
    +
    +

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

    +
    +
    var remove_item
    +
    +

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

    +
    +
    var reply_all_to_item
    +
    +

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

    +
    +
    var reply_to_item
    +
    +

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

    +
    +
    var success_read_receipt
    +
    +

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

    +
    +
    var tentatively_accept_item
    +
    +

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

    +
    +
    +

    Inherited members

    + +
    class Room (**kwargs) @@ -5978,6 +6467,59 @@

    Inherited members

    +
    +class SuppressReadReceipt +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class SuppressReadReceipt(EWSElement):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suppressreadreceipt"""
    +    ELEMENT_NAME = 'SuppressReadReceipt'
    +    FIELDS = Fields(
    +        ReferenceItemIdField('reference_item_id', field_uri='item:ReferenceItemId'),
    +    )
    +
    +    __slots__ = tuple(f.name for f in FIELDS)
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    var FIELDS
    +
    +
    +
    +
    +

    Instance variables

    +
    +
    var reference_item_id
    +
    +

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

    +
    +
    +

    Inherited members

    + +
    class TimeWindow (**kwargs) @@ -6676,6 +7218,14 @@

    Index

  • Classes

    • +

      AcceptSharingInvitation

      + +
    • +
    • AlternateId

      • ELEMENT_NAME
      • @@ -6798,6 +7348,23 @@

        CompleteName

        + + +
      • ConversationId

        • ELEMENT_NAME
        • @@ -7168,6 +7735,44 @@

          ReminderMessageData

          + + +
        • +

          RemoveItem

          + +
        • +
        • +

          ResponseObjects

          + +
        • +
        • Room

          • ELEMENT_NAME
          • @@ -7220,6 +7825,14 @@

            SuppressReadReceipt

            + + +
          • TimeWindow

            • ELEMENT_NAME
            • @@ -7284,7 +7897,7 @@

              -

              Generated by pdoc 0.8.4.

              +

              Generated by pdoc 0.9.1.

              \ No newline at end of file diff --git a/docs/exchangelib/protocol.html b/docs/exchangelib/protocol.html index 0184fd8e..0098df22 100644 --- a/docs/exchangelib/protocol.html +++ b/docs/exchangelib/protocol.html @@ -3,16 +3,16 @@ - + exchangelib.protocol API documentation - + - + @@ -1599,7 +1599,7 @@

              Methods

  • class CachingProtocol -(...) +(*args, **kwargs)

    type(object_or_name, bases, dict) @@ -2742,7 +2742,7 @@

    -

    Generated by pdoc 0.8.4.

    +

    Generated by pdoc 0.9.1.

    \ No newline at end of file diff --git a/docs/exchangelib/queryset.html b/docs/exchangelib/queryset.html index 43cab2c9..5d5eaf1a 100644 --- a/docs/exchangelib/queryset.html +++ b/docs/exchangelib/queryset.html @@ -3,16 +3,16 @@ - + exchangelib.queryset API documentation - + - + @@ -799,7 +799,7 @@

    Module exchangelib.queryset

    # value. if isinstance(item, Exception): return _default_field_value(field_order.field_path.field) - val = field_order.field_path.get_value(item) + val = field_order.field_path.get_sort_value(item) if val is None: return _default_field_value(field_order.field_path.field) return val @@ -2389,7 +2389,7 @@

    -

    Generated by pdoc 0.8.4.

    +

    Generated by pdoc 0.9.1.

    \ No newline at end of file diff --git a/docs/exchangelib/recurrence.html b/docs/exchangelib/recurrence.html index 26763d0d..30d00758 100644 --- a/docs/exchangelib/recurrence.html +++ b/docs/exchangelib/recurrence.html @@ -3,16 +3,16 @@ - + exchangelib.recurrence API documentation - + - + @@ -28,7 +28,7 @@

    Module exchangelib.recurrence

    import logging
     
    -from .fields import IntegerField, EnumField, EnumListField, DateField, DateTimeField, EWSElementField, \
    +from .fields import IntegerField, EnumField, EnumListField, DateOrDateTimeField, DateTimeField, EWSElementField, \
         IdElementField, MONTHS, WEEK_NUMBERS, WEEKDAYS
     from .properties import EWSElement, IdChangeKeyMixIn, ItemId, Fields
     
    @@ -52,6 +52,11 @@ 

    Module exchangelib.recurrence

    __slots__ = tuple() +class Regeneration(Pattern): + """Base class for all classes implementing recurring regeneration elements""" + __slots__ = tuple() + + class AbsoluteYearlyPattern(Pattern): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/absoluteyearlyrecurrence """ @@ -186,6 +191,66 @@

    Module exchangelib.recurrence

    return 'Occurs every %s day(s)' % self.interval +class YearlyRegeneration(Regeneration): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/yearlyregeneration""" + ELEMENT_NAME = 'YearlyRegeneration' + + FIELDS = Fields( + # Interval, in years + IntegerField('interval', field_uri='Interval', min=1, is_required=True), + ) + + __slots__ = tuple(f.name for f in FIELDS) + + def __str__(self): + return 'Regenerates every %s year(s)' % self.interval + + +class MonthlyRegeneration(Regeneration): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/monthlyregeneration""" + ELEMENT_NAME = 'MonthlyRegeneration' + + FIELDS = Fields( + # Interval, in months + IntegerField('interval', field_uri='Interval', min=1, is_required=True), + ) + + __slots__ = tuple(f.name for f in FIELDS) + + def __str__(self): + return 'Regenerates every %s month(s)' % self.interval + + +class WeeklyRegeneration(Regeneration): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/weeklyregeneration""" + ELEMENT_NAME = 'WeeklyRegeneration' + + FIELDS = Fields( + # Interval, in weeks + IntegerField('interval', field_uri='Interval', min=1, is_required=True), + ) + + __slots__ = tuple(f.name for f in FIELDS) + + def __str__(self): + return 'Regenerates every %s week(s)' % self.interval + + +class DailyRegeneration(Regeneration): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dailyregeneration""" + ELEMENT_NAME = 'DailyRegeneration' + + FIELDS = Fields( + # Interval, in days + IntegerField('interval', field_uri='Interval', min=1, is_required=True), + ) + + __slots__ = tuple(f.name for f in FIELDS) + + def __str__(self): + return 'Regenerates every %s day(s)' % self.interval + + class Boundary(EWSElement): """Base class for all classes implementing recurring boundary elements""" __slots__ = tuple() @@ -196,40 +261,49 @@

    Module exchangelib.recurrence

    ELEMENT_NAME = 'NoEndRecurrence' FIELDS = Fields( - # Start date, as EWSDate - DateField('start', field_uri='StartDate', is_required=True), + # Start date, as EWSDate or EWSDateTime + DateOrDateTimeField('start', field_uri='StartDate', is_required=True), ) __slots__ = tuple(f.name for f in FIELDS) + def __str__(self): + return 'Starts on %s' % self.start + class EndDatePattern(Boundary): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/enddaterecurrence""" ELEMENT_NAME = 'EndDateRecurrence' FIELDS = Fields( - # Start date, as EWSDate - DateField('start', field_uri='StartDate', is_required=True), + # Start date, as EWSDate or EWSDateTime + DateOrDateTimeField('start', field_uri='StartDate', is_required=True), # End date, as EWSDate - DateField('end', field_uri='EndDate', is_required=True), + DateOrDateTimeField('end', field_uri='EndDate', is_required=True), ) __slots__ = tuple(f.name for f in FIELDS) + def __str__(self): + return 'Starts on %s, ends on %s' % (self.start, self.end) + class NumberedPattern(Boundary): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/numberedrecurrence""" ELEMENT_NAME = 'NumberedRecurrence' FIELDS = Fields( - # Start date, as EWSDate - DateField('start', field_uri='StartDate', is_required=True), + # Start date, as EWSDate or EWSDateTime + DateOrDateTimeField('start', field_uri='StartDate', is_required=True), # The number of occurrences in this pattern, in range 1 -> 999 IntegerField('number', field_uri='NumberOfOccurrences', min=1, max=999, is_required=True), ) __slots__ = tuple(f.name for f in FIELDS) + def __str__(self): + return 'Starts on %s and occurs %s times' % (self.start, self.number) + class Occurrence(IdChangeKeyMixIn): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/occurrence""" @@ -280,6 +354,7 @@

    Module exchangelib.recurrence

    PATTERN_CLASSES = AbsoluteYearlyPattern, RelativeYearlyPattern, AbsoluteMonthlyPattern, RelativeMonthlyPattern, \ WeeklyPattern, DailyPattern +REGENERATION_CLASSES = YearlyRegeneration, MonthlyRegeneration, WeeklyRegeneration, DailyRegeneration BOUNDARY_CLASSES = NoEndPattern, EndDatePattern, NumberedPattern @@ -292,6 +367,7 @@

    Module exchangelib.recurrence

    EWSElementField('pattern', value_cls=Pattern), EWSElementField('boundary', value_cls=Boundary), ) + PATTERN_CLASSES = PATTERN_CLASSES __slots__ = tuple(f.name for f in FIELDS) @@ -315,7 +391,7 @@

    Module exchangelib.recurrence

    @classmethod def from_xml(cls, elem, account): - for pattern_cls in PATTERN_CLASSES: + for pattern_cls in cls.PATTERN_CLASSES: pattern_elem = elem.find(pattern_cls.response_tag()) if pattern_elem is None: continue @@ -334,7 +410,15 @@

    Module exchangelib.recurrence

    return cls(pattern=pattern, boundary=boundary) def __str__(self): - return 'Pattern: %s, Boundary: %s' % (self.pattern, self.boundary)
    + return 'Pattern: %s, Boundary: %s' % (self.pattern, self.boundary) + + +class TaskRecurrence(Recurrence): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recurrence-taskrecurrencetype + """ + PATTERN_CLASSES = PATTERN_CLASSES + REGENERATION_CLASSES + + __slots__ = tuple()
    @@ -575,6 +659,66 @@

    Inherited members

    +
    +class DailyRegeneration +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class DailyRegeneration(Regeneration):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/dailyregeneration"""
    +    ELEMENT_NAME = 'DailyRegeneration'
    +
    +    FIELDS = Fields(
    +        # Interval, in days
    +        IntegerField('interval', field_uri='Interval', min=1, is_required=True),
    +    )
    +
    +    __slots__ = tuple(f.name for f in FIELDS)
    +
    +    def __str__(self):
    +        return 'Regenerates every %s day(s)' % self.interval
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    var FIELDS
    +
    +
    +
    +
    +

    Instance variables

    +
    +
    var interval
    +
    +

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

    +
    +
    +

    Inherited members

    + +
    class DeletedOccurrence (**kwargs) @@ -645,13 +789,16 @@

    Inherited members

    ELEMENT_NAME = 'EndDateRecurrence' FIELDS = Fields( - # Start date, as EWSDate - DateField('start', field_uri='StartDate', is_required=True), + # Start date, as EWSDate or EWSDateTime + DateOrDateTimeField('start', field_uri='StartDate', is_required=True), # End date, as EWSDate - DateField('end', field_uri='EndDate', is_required=True), + DateOrDateTimeField('end', field_uri='EndDate', is_required=True), ) - __slots__ = tuple(f.name for f in FIELDS)
    + __slots__ = tuple(f.name for f in FIELDS) + + def __str__(self): + return 'Starts on %s, ends on %s' % (self.start, self.end)

    Ancestors

      @@ -780,6 +927,66 @@

      Inherited members

    +
    +class MonthlyRegeneration +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class MonthlyRegeneration(Regeneration):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/monthlyregeneration"""
    +    ELEMENT_NAME = 'MonthlyRegeneration'
    +
    +    FIELDS = Fields(
    +        # Interval, in months
    +        IntegerField('interval', field_uri='Interval', min=1, is_required=True),
    +    )
    +
    +    __slots__ = tuple(f.name for f in FIELDS)
    +
    +    def __str__(self):
    +        return 'Regenerates every %s month(s)' % self.interval
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    var FIELDS
    +
    +
    +
    +
    +

    Instance variables

    +
    +
    var interval
    +
    +

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

    +
    +
    +

    Inherited members

    + +
    class NoEndPattern (**kwargs) @@ -795,11 +1002,14 @@

    Inherited members

    ELEMENT_NAME = 'NoEndRecurrence' FIELDS = Fields( - # Start date, as EWSDate - DateField('start', field_uri='StartDate', is_required=True), + # Start date, as EWSDate or EWSDateTime + DateOrDateTimeField('start', field_uri='StartDate', is_required=True), ) - __slots__ = tuple(f.name for f in FIELDS)
    + __slots__ = tuple(f.name for f in FIELDS) + + def __str__(self): + return 'Starts on %s' % self.start

    Ancestors

      @@ -851,13 +1061,16 @@

      Inherited members

      ELEMENT_NAME = 'NumberedRecurrence' FIELDS = Fields( - # Start date, as EWSDate - DateField('start', field_uri='StartDate', is_required=True), + # Start date, as EWSDate or EWSDateTime + DateOrDateTimeField('start', field_uri='StartDate', is_required=True), # The number of occurrences in this pattern, in range 1 -> 999 IntegerField('number', field_uri='NumberOfOccurrences', min=1, max=999, is_required=True), ) - __slots__ = tuple(f.name for f in FIELDS)
      + __slots__ = tuple(f.name for f in FIELDS) + + def __str__(self): + return 'Starts on %s and occurs %s times' % (self.start, self.number)

      Ancestors

        @@ -1001,6 +1214,7 @@

        Subclasses

      • AbsoluteMonthlyPattern
      • AbsoluteYearlyPattern
      • DailyPattern
      • +
      • Regeneration
      • RelativeMonthlyPattern
      • RelativeYearlyPattern
      • WeeklyPattern
      • @@ -1036,6 +1250,7 @@

        Inherited members

        EWSElementField('pattern', value_cls=Pattern), EWSElementField('boundary', value_cls=Boundary), ) + PATTERN_CLASSES = PATTERN_CLASSES __slots__ = tuple(f.name for f in FIELDS) @@ -1059,7 +1274,7 @@

        Inherited members

        @classmethod def from_xml(cls, elem, account): - for pattern_cls in PATTERN_CLASSES: + for pattern_cls in cls.PATTERN_CLASSES: pattern_elem = elem.find(pattern_cls.response_tag()) if pattern_elem is None: continue @@ -1084,6 +1299,10 @@

        Ancestors

        +

        Subclasses

        +

        Class variables

        var ELEMENT_NAME
        @@ -1094,6 +1313,10 @@

        Class variables

        +
        var PATTERN_CLASSES
        +
        +
        +

        Static methods

        @@ -1108,7 +1331,7 @@

        Static methods

        @classmethod
         def from_xml(cls, elem, account):
        -    for pattern_cls in PATTERN_CLASSES:
        +    for pattern_cls in cls.PATTERN_CLASSES:
                 pattern_elem = elem.find(pattern_cls.response_tag())
                 if pattern_elem is None:
                     continue
        @@ -1151,6 +1374,44 @@ 

        Inherited members

      +
      +class Regeneration +(**kwargs) +
      +
      +

      Base class for all classes implementing recurring regeneration elements

      +
      + +Expand source code + +
      class Regeneration(Pattern):
      +    """Base class for all classes implementing recurring regeneration elements"""
      +    __slots__ = tuple()
      +
      +

      Ancestors

      + +

      Subclasses

      + +

      Inherited members

      + +
      class RelativeMonthlyPattern (**kwargs) @@ -1309,6 +1570,49 @@

      Inherited members

    +
    +class TaskRecurrence +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class TaskRecurrence(Recurrence):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/recurrence-taskrecurrencetype
    +    """
    +    PATTERN_CLASSES = PATTERN_CLASSES + REGENERATION_CLASSES
    +
    +    __slots__ = tuple()
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var PATTERN_CLASSES
    +
    +
    +
    +
    +

    Inherited members

    + +
    class WeeklyPattern (**kwargs) @@ -1388,6 +1692,126 @@

    Inherited members

    +
    +class WeeklyRegeneration +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class WeeklyRegeneration(Regeneration):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/weeklyregeneration"""
    +    ELEMENT_NAME = 'WeeklyRegeneration'
    +
    +    FIELDS = Fields(
    +        # Interval, in weeks
    +        IntegerField('interval', field_uri='Interval', min=1, is_required=True),
    +    )
    +
    +    __slots__ = tuple(f.name for f in FIELDS)
    +
    +    def __str__(self):
    +        return 'Regenerates every %s week(s)' % self.interval
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    var FIELDS
    +
    +
    +
    +
    +

    Instance variables

    +
    +
    var interval
    +
    +

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

    +
    +
    +

    Inherited members

    + +
    +
    +class YearlyRegeneration +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class YearlyRegeneration(Regeneration):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/yearlyregeneration"""
    +    ELEMENT_NAME = 'YearlyRegeneration'
    +
    +    FIELDS = Fields(
    +        # Interval, in years
    +        IntegerField('interval', field_uri='Interval', min=1, is_required=True),
    +    )
    +
    +    __slots__ = tuple(f.name for f in FIELDS)
    +
    +    def __str__(self):
    +        return 'Regenerates every %s year(s)' % self.interval
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    var FIELDS
    +
    +
    +
    +
    +

    Instance variables

    +
    +
    var interval
    +
    +

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

    +
    +
    +

    Inherited members

    + +
    @@ -1434,6 +1858,14 @@

    DailyRegeneration

    + + +
  • DeletedOccurrence

    • ELEMENT_NAME
    • @@ -1463,6 +1895,14 @@

      MonthlyRegeneration

      + + +
    • NoEndPattern

      • ELEMENT_NAME
      • @@ -1495,15 +1935,19 @@

        Recurrence

        - \ No newline at end of file diff --git a/docs/exchangelib/restriction.html b/docs/exchangelib/restriction.html index 9706a78e..e7be82df 100644 --- a/docs/exchangelib/restriction.html +++ b/docs/exchangelib/restriction.html @@ -3,16 +3,16 @@ - + exchangelib.restriction API documentation - + - + @@ -1672,7 +1672,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/archive_item.html b/docs/exchangelib/services/archive_item.html index a0a7c5c4..c8f92c91 100644 --- a/docs/exchangelib/services/archive_item.html +++ b/docs/exchangelib/services/archive_item.html @@ -3,16 +3,16 @@ - + exchangelib.services.archive_item API documentation - + - + @@ -241,7 +241,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/common.html b/docs/exchangelib/services/common.html index 2959dfc4..8983205a 100644 --- a/docs/exchangelib/services/common.html +++ b/docs/exchangelib/services/common.html @@ -3,16 +3,16 @@ - + exchangelib.services.common API documentation - + - + @@ -1598,7 +1598,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/convert_id.html b/docs/exchangelib/services/convert_id.html index 411cbb3f..2ff1282c 100644 --- a/docs/exchangelib/services/convert_id.html +++ b/docs/exchangelib/services/convert_id.html @@ -3,16 +3,16 @@ - + exchangelib.services.convert_id API documentation - + - + @@ -252,7 +252,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/copy_item.html b/docs/exchangelib/services/copy_item.html index f4defba1..307a7e86 100644 --- a/docs/exchangelib/services/copy_item.html +++ b/docs/exchangelib/services/copy_item.html @@ -3,16 +3,16 @@ - + exchangelib.services.copy_item API documentation - + - + @@ -107,7 +107,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/create_attachment.html b/docs/exchangelib/services/create_attachment.html index 9ccf7e86..38cda87e 100644 --- a/docs/exchangelib/services/create_attachment.html +++ b/docs/exchangelib/services/create_attachment.html @@ -3,16 +3,16 @@ - + exchangelib.services.create_attachment API documentation - + - + @@ -210,7 +210,7 @@

        \ No newline at end of file diff --git a/docs/exchangelib/services/create_folder.html b/docs/exchangelib/services/create_folder.html index 7ee7dbb6..7ed450fc 100644 --- a/docs/exchangelib/services/create_folder.html +++ b/docs/exchangelib/services/create_folder.html @@ -3,16 +3,16 @@ - + exchangelib.services.create_folder API documentation - + - + @@ -193,7 +193,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/create_item.html b/docs/exchangelib/services/create_item.html index 3285a4e6..aeb507dd 100644 --- a/docs/exchangelib/services/create_item.html +++ b/docs/exchangelib/services/create_item.html @@ -3,16 +3,16 @@ - + exchangelib.services.create_item API documentation - + - + @@ -384,7 +384,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/delete_attachment.html b/docs/exchangelib/services/delete_attachment.html index 125125cd..c8a6a0fc 100644 --- a/docs/exchangelib/services/delete_attachment.html +++ b/docs/exchangelib/services/delete_attachment.html @@ -3,16 +3,16 @@ - + exchangelib.services.delete_attachment API documentation - + - + @@ -195,7 +195,7 @@

        \ No newline at end of file diff --git a/docs/exchangelib/services/delete_folder.html b/docs/exchangelib/services/delete_folder.html index 7bee1ccb..a4e866e9 100644 --- a/docs/exchangelib/services/delete_folder.html +++ b/docs/exchangelib/services/delete_folder.html @@ -3,16 +3,16 @@ - + exchangelib.services.delete_folder API documentation - + - + @@ -166,7 +166,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/delete_item.html b/docs/exchangelib/services/delete_item.html index 0c80b949..d512900a 100644 --- a/docs/exchangelib/services/delete_item.html +++ b/docs/exchangelib/services/delete_item.html @@ -3,16 +3,16 @@ - + exchangelib.services.delete_item API documentation - + - + @@ -308,7 +308,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/empty_folder.html b/docs/exchangelib/services/empty_folder.html index 78406715..27520c8a 100644 --- a/docs/exchangelib/services/empty_folder.html +++ b/docs/exchangelib/services/empty_folder.html @@ -3,16 +3,16 @@ - + exchangelib.services.empty_folder API documentation - + - + @@ -189,7 +189,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/expand_dl.html b/docs/exchangelib/services/expand_dl.html index af4a88e0..76aeb3b5 100644 --- a/docs/exchangelib/services/expand_dl.html +++ b/docs/exchangelib/services/expand_dl.html @@ -3,16 +3,16 @@ - + exchangelib.services.expand_dl API documentation - + - + @@ -192,7 +192,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/export_items.html b/docs/exchangelib/services/export_items.html index a4b10e85..a3f419c9 100644 --- a/docs/exchangelib/services/export_items.html +++ b/docs/exchangelib/services/export_items.html @@ -3,16 +3,16 @@ - + exchangelib.services.export_items API documentation - + - + @@ -187,7 +187,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/find_folder.html b/docs/exchangelib/services/find_folder.html index 91b9434c..145f46de 100644 --- a/docs/exchangelib/services/find_folder.html +++ b/docs/exchangelib/services/find_folder.html @@ -3,16 +3,16 @@ - + exchangelib.services.find_folder API documentation - + - + @@ -338,7 +338,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/find_item.html b/docs/exchangelib/services/find_item.html index 938ea20a..d40b75b3 100644 --- a/docs/exchangelib/services/find_item.html +++ b/docs/exchangelib/services/find_item.html @@ -3,16 +3,16 @@ - + exchangelib.services.find_item API documentation - + - + @@ -367,7 +367,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/find_people.html b/docs/exchangelib/services/find_people.html index ea65a004..dc4678f5 100644 --- a/docs/exchangelib/services/find_people.html +++ b/docs/exchangelib/services/find_people.html @@ -3,16 +3,16 @@ - + exchangelib.services.find_people API documentation - + - + @@ -451,7 +451,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_attachment.html b/docs/exchangelib/services/get_attachment.html index ea6b00ad..55bbe01b 100644 --- a/docs/exchangelib/services/get_attachment.html +++ b/docs/exchangelib/services/get_attachment.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_attachment API documentation - + - + @@ -323,7 +323,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_delegate.html b/docs/exchangelib/services/get_delegate.html index d3b1a8cf..efaeeb84 100644 --- a/docs/exchangelib/services/get_delegate.html +++ b/docs/exchangelib/services/get_delegate.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_delegate API documentation - + - + @@ -281,7 +281,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_folder.html b/docs/exchangelib/services/get_folder.html index 1b750048..1b884c80 100644 --- a/docs/exchangelib/services/get_folder.html +++ b/docs/exchangelib/services/get_folder.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_folder API documentation - + - + @@ -269,7 +269,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_item.html b/docs/exchangelib/services/get_item.html index 2a772b52..d4f5542b 100644 --- a/docs/exchangelib/services/get_item.html +++ b/docs/exchangelib/services/get_item.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_item API documentation - + - + @@ -235,7 +235,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_mail_tips.html b/docs/exchangelib/services/get_mail_tips.html index ebc37946..e3596a15 100644 --- a/docs/exchangelib/services/get_mail_tips.html +++ b/docs/exchangelib/services/get_mail_tips.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_mail_tips API documentation - + - + @@ -221,7 +221,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_persona.html b/docs/exchangelib/services/get_persona.html index 75384a61..1e98906c 100644 --- a/docs/exchangelib/services/get_persona.html +++ b/docs/exchangelib/services/get_persona.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_persona API documentation - + - + @@ -190,7 +190,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_room_lists.html b/docs/exchangelib/services/get_room_lists.html index 22dd8bf7..8d12ce7a 100644 --- a/docs/exchangelib/services/get_room_lists.html +++ b/docs/exchangelib/services/get_room_lists.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_room_lists API documentation - + - + @@ -172,7 +172,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_rooms.html b/docs/exchangelib/services/get_rooms.html index 75874d65..0584df55 100644 --- a/docs/exchangelib/services/get_rooms.html +++ b/docs/exchangelib/services/get_rooms.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_rooms API documentation - + - + @@ -178,7 +178,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_searchable_mailboxes.html b/docs/exchangelib/services/get_searchable_mailboxes.html index b65a386e..d35fe689 100644 --- a/docs/exchangelib/services/get_searchable_mailboxes.html +++ b/docs/exchangelib/services/get_searchable_mailboxes.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_searchable_mailboxes API documentation - + - + @@ -264,7 +264,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_server_time_zones.html b/docs/exchangelib/services/get_server_time_zones.html index 35ecf8c2..464cc407 100644 --- a/docs/exchangelib/services/get_server_time_zones.html +++ b/docs/exchangelib/services/get_server_time_zones.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_server_time_zones API documentation - + - + @@ -365,7 +365,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_user_availability.html b/docs/exchangelib/services/get_user_availability.html index 79ada0f3..42262fef 100644 --- a/docs/exchangelib/services/get_user_availability.html +++ b/docs/exchangelib/services/get_user_availability.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_user_availability API documentation - + - + @@ -246,7 +246,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/get_user_oof_settings.html b/docs/exchangelib/services/get_user_oof_settings.html index 44f3c814..14b80d69 100644 --- a/docs/exchangelib/services/get_user_oof_settings.html +++ b/docs/exchangelib/services/get_user_oof_settings.html @@ -3,16 +3,16 @@ - + exchangelib.services.get_user_oof_settings API documentation - + - + @@ -206,7 +206,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/index.html b/docs/exchangelib/services/index.html index 9eeaad46..1500ec27 100644 --- a/docs/exchangelib/services/index.html +++ b/docs/exchangelib/services/index.html @@ -3,16 +3,16 @@ - + exchangelib.services API documentation - + - + @@ -4591,22 +4591,16 @@

        Inherited members

        if item.__class__ == CalendarItem: # For CalendarItem items where we update 'start' or 'end', we want to update internal timezone fields item.clean_timezone_fields(version=self.account.version) # Possibly also sets timezone values - meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields() - if self.account.version.build < EXCHANGE_2010: - if 'start' in fieldnames_copy or 'end' in fieldnames_copy: - fieldnames_copy.append(meeting_tz_field.name) - else: - if 'start' in fieldnames_copy: - fieldnames_copy.append(start_tz_field.name) - if 'end' in fieldnames_copy: - fieldnames_copy.append(end_tz_field.name) - else: - meeting_tz_field, start_tz_field, end_tz_field = None, None, None + for field_name in ('start', 'end'): + if field_name in fieldnames_copy: + tz_field_name = item.tz_field_for_field_name(field_name).name + if tz_field_name not in fieldnames_copy: + fieldnames_copy.append(tz_field_name) for field in self._sorted_fields(item_model=item.__class__, fieldnames=fieldnames_copy): if field.is_read_only: raise ValueError('%s is a read-only field' % field.name) - value = self._get_item_value(item, field, meeting_tz_field, start_tz_field, end_tz_field) + value = self._get_item_value(item, field) if value is None or (field.is_list and not value): # A value of None or [] means we want to remove this field from the item for elem in self._get_delete_item_elems(field=field): @@ -4615,22 +4609,17 @@

        Inherited members

        for elem in self._get_set_item_elems(item_model=item.__class__, field=field, value=value): yield elem - def _get_item_value(self, item, field, meeting_tz_field, start_tz_field, end_tz_field): + def _get_item_value(self, item, field): from ..items import CalendarItem value = field.clean(getattr(item, field.name), version=self.account.version) # Make sure the value is OK if item.__class__ == CalendarItem: # For CalendarItem items where we update 'start' or 'end', we want to send values in the local timezone - if field.name in ('start', 'end') and type(value) == EWSDate: - # EWS always expects a datetime - return item.date_to_datetime(field_name=field.name) - if self.account.version.build < EXCHANGE_2010: - if field.name in ('start', 'end'): - value = value.astimezone(getattr(item, meeting_tz_field.name)) - else: - if field.name == 'start': - value = value.astimezone(getattr(item, start_tz_field.name)) - elif field.name == 'end': - value = value.astimezone(getattr(item, end_tz_field.name)) + if field.name in ('start', 'end'): + if type(value) == EWSDate: + # EWS always expects a datetime + return item.date_to_datetime(field_name=field.name) + tz_field_name = item.tz_field_for_field_name(field.name).name + return value.astimezone(getattr(item, tz_field_name)) return value def _get_delete_item_elems(self, field): @@ -5336,7 +5325,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/move_item.html b/docs/exchangelib/services/move_item.html index e5279f98..8863aca4 100644 --- a/docs/exchangelib/services/move_item.html +++ b/docs/exchangelib/services/move_item.html @@ -3,16 +3,16 @@ - + exchangelib.services.move_item API documentation - + - + @@ -200,7 +200,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/resolve_names.html b/docs/exchangelib/services/resolve_names.html index 67574677..5dcd55f9 100644 --- a/docs/exchangelib/services/resolve_names.html +++ b/docs/exchangelib/services/resolve_names.html @@ -3,16 +3,16 @@ - + exchangelib.services.resolve_names API documentation - + - + @@ -318,7 +318,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/send_item.html b/docs/exchangelib/services/send_item.html index 10089816..ef4b51c4 100644 --- a/docs/exchangelib/services/send_item.html +++ b/docs/exchangelib/services/send_item.html @@ -3,16 +3,16 @@ - + exchangelib.services.send_item API documentation - + - + @@ -196,7 +196,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/set_user_oof_settings.html b/docs/exchangelib/services/set_user_oof_settings.html index b1ba1d25..0d5ad5fd 100644 --- a/docs/exchangelib/services/set_user_oof_settings.html +++ b/docs/exchangelib/services/set_user_oof_settings.html @@ -3,16 +3,16 @@ - + exchangelib.services.set_user_oof_settings API documentation - + - + @@ -199,7 +199,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/update_folder.html b/docs/exchangelib/services/update_folder.html index 1d4d59d1..6f7e7643 100644 --- a/docs/exchangelib/services/update_folder.html +++ b/docs/exchangelib/services/update_folder.html @@ -3,16 +3,16 @@ - + exchangelib.services.update_folder API documentation - + - + @@ -311,7 +311,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/update_item.html b/docs/exchangelib/services/update_item.html index 6353edeb..4abc1825 100644 --- a/docs/exchangelib/services/update_item.html +++ b/docs/exchangelib/services/update_item.html @@ -3,16 +3,16 @@ - + exchangelib.services.update_item API documentation - + - + @@ -31,7 +31,7 @@

        Module exchangelib.services.update_item

        from ..ewsdatetime import EWSDate from ..util import create_element, set_xml_value, MNS -from ..version import EXCHANGE_2010, EXCHANGE_2013_SP1 +from ..version import EXCHANGE_2013_SP1 from .common import EWSAccountService, EWSPooledMixIn, to_item_id log = logging.getLogger(__name__) @@ -104,22 +104,16 @@

        Module exchangelib.services.update_item

        if item.__class__ == CalendarItem: # For CalendarItem items where we update 'start' or 'end', we want to update internal timezone fields item.clean_timezone_fields(version=self.account.version) # Possibly also sets timezone values - meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields() - if self.account.version.build < EXCHANGE_2010: - if 'start' in fieldnames_copy or 'end' in fieldnames_copy: - fieldnames_copy.append(meeting_tz_field.name) - else: - if 'start' in fieldnames_copy: - fieldnames_copy.append(start_tz_field.name) - if 'end' in fieldnames_copy: - fieldnames_copy.append(end_tz_field.name) - else: - meeting_tz_field, start_tz_field, end_tz_field = None, None, None + for field_name in ('start', 'end'): + if field_name in fieldnames_copy: + tz_field_name = item.tz_field_for_field_name(field_name).name + if tz_field_name not in fieldnames_copy: + fieldnames_copy.append(tz_field_name) for field in self._sorted_fields(item_model=item.__class__, fieldnames=fieldnames_copy): if field.is_read_only: raise ValueError('%s is a read-only field' % field.name) - value = self._get_item_value(item, field, meeting_tz_field, start_tz_field, end_tz_field) + value = self._get_item_value(item, field) if value is None or (field.is_list and not value): # A value of None or [] means we want to remove this field from the item for elem in self._get_delete_item_elems(field=field): @@ -128,22 +122,17 @@

        Module exchangelib.services.update_item

        for elem in self._get_set_item_elems(item_model=item.__class__, field=field, value=value): yield elem - def _get_item_value(self, item, field, meeting_tz_field, start_tz_field, end_tz_field): + def _get_item_value(self, item, field): from ..items import CalendarItem value = field.clean(getattr(item, field.name), version=self.account.version) # Make sure the value is OK if item.__class__ == CalendarItem: # For CalendarItem items where we update 'start' or 'end', we want to send values in the local timezone - if field.name in ('start', 'end') and type(value) == EWSDate: - # EWS always expects a datetime - return item.date_to_datetime(field_name=field.name) - if self.account.version.build < EXCHANGE_2010: - if field.name in ('start', 'end'): - value = value.astimezone(getattr(item, meeting_tz_field.name)) - else: - if field.name == 'start': - value = value.astimezone(getattr(item, start_tz_field.name)) - elif field.name == 'end': - value = value.astimezone(getattr(item, end_tz_field.name)) + if field.name in ('start', 'end'): + if type(value) == EWSDate: + # EWS always expects a datetime + return item.date_to_datetime(field_name=field.name) + tz_field_name = item.tz_field_for_field_name(field.name).name + return value.astimezone(getattr(item, tz_field_name)) return value def _get_delete_item_elems(self, field): @@ -311,22 +300,16 @@

        Classes

        if item.__class__ == CalendarItem: # For CalendarItem items where we update 'start' or 'end', we want to update internal timezone fields item.clean_timezone_fields(version=self.account.version) # Possibly also sets timezone values - meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields() - if self.account.version.build < EXCHANGE_2010: - if 'start' in fieldnames_copy or 'end' in fieldnames_copy: - fieldnames_copy.append(meeting_tz_field.name) - else: - if 'start' in fieldnames_copy: - fieldnames_copy.append(start_tz_field.name) - if 'end' in fieldnames_copy: - fieldnames_copy.append(end_tz_field.name) - else: - meeting_tz_field, start_tz_field, end_tz_field = None, None, None + for field_name in ('start', 'end'): + if field_name in fieldnames_copy: + tz_field_name = item.tz_field_for_field_name(field_name).name + if tz_field_name not in fieldnames_copy: + fieldnames_copy.append(tz_field_name) for field in self._sorted_fields(item_model=item.__class__, fieldnames=fieldnames_copy): if field.is_read_only: raise ValueError('%s is a read-only field' % field.name) - value = self._get_item_value(item, field, meeting_tz_field, start_tz_field, end_tz_field) + value = self._get_item_value(item, field) if value is None or (field.is_list and not value): # A value of None or [] means we want to remove this field from the item for elem in self._get_delete_item_elems(field=field): @@ -335,22 +318,17 @@

        Classes

        for elem in self._get_set_item_elems(item_model=item.__class__, field=field, value=value): yield elem - def _get_item_value(self, item, field, meeting_tz_field, start_tz_field, end_tz_field): + def _get_item_value(self, item, field): from ..items import CalendarItem value = field.clean(getattr(item, field.name), version=self.account.version) # Make sure the value is OK if item.__class__ == CalendarItem: # For CalendarItem items where we update 'start' or 'end', we want to send values in the local timezone - if field.name in ('start', 'end') and type(value) == EWSDate: - # EWS always expects a datetime - return item.date_to_datetime(field_name=field.name) - if self.account.version.build < EXCHANGE_2010: - if field.name in ('start', 'end'): - value = value.astimezone(getattr(item, meeting_tz_field.name)) - else: - if field.name == 'start': - value = value.astimezone(getattr(item, start_tz_field.name)) - elif field.name == 'end': - value = value.astimezone(getattr(item, end_tz_field.name)) + if field.name in ('start', 'end'): + if type(value) == EWSDate: + # EWS always expects a datetime + return item.date_to_datetime(field_name=field.name) + tz_field_name = item.tz_field_for_field_name(field.name).name + return value.astimezone(getattr(item, tz_field_name)) return value def _get_delete_item_elems(self, field): @@ -582,7 +560,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/services/upload_items.html b/docs/exchangelib/services/upload_items.html index dc3b48ba..9daa4c8f 100644 --- a/docs/exchangelib/services/upload_items.html +++ b/docs/exchangelib/services/upload_items.html @@ -3,16 +3,16 @@ - + exchangelib.services.upload_items API documentation - + - + @@ -253,7 +253,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/settings.html b/docs/exchangelib/settings.html index a50e5b07..86927dec 100644 --- a/docs/exchangelib/settings.html +++ b/docs/exchangelib/settings.html @@ -3,16 +3,16 @@ - + exchangelib.settings API documentation - + - + @@ -410,7 +410,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/transport.html b/docs/exchangelib/transport.html index f93a9d7a..b8317c22 100644 --- a/docs/exchangelib/transport.html +++ b/docs/exchangelib/transport.html @@ -3,16 +3,16 @@ - + exchangelib.transport API documentation - + - + @@ -557,7 +557,7 @@

        Index

        \ No newline at end of file diff --git a/docs/exchangelib/util.html b/docs/exchangelib/util.html index 47840035..65c596d3 100644 --- a/docs/exchangelib/util.html +++ b/docs/exchangelib/util.html @@ -3,16 +3,16 @@ - + exchangelib.util API documentation - + - + @@ -45,7 +45,6 @@

        Module exchangelib.util

        import lxml.etree # nosec from defusedxml.expatreader import DefusedExpatParser from defusedxml.sax import _InputSource -import dns.resolver import isodate from oauthlib.oauth2 import TokenExpiredError from pygments import highlight @@ -516,18 +515,22 @@

        Module exchangelib.util

        return res -def is_xml(text): - """Helper function. Lightweight test if response is an XML doc +def is_xml(text, expected_prefix=b'<?xml'): + """Lightweight test if response is an XML doc. It's better to be fast than correct here. Args: - text: + text: The string to check + expected_prefix: What to search for in the start if the string """ # BOM_UTF8 is an UTF-8 byte order mark which may precede the XML from an Exchange server bom_len = len(BOM_UTF8) + prefix_len = len(expected_prefix) if text[:bom_len] == BOM_UTF8: - return text[bom_len:bom_len + 5] == b'<?xml' - return text[:5] == b'<?xml' + prefix = text[bom_len:bom_len + prefix_len] + else: + prefix = text[:prefix_len] + return prefix == expected_prefix class PrettyXmlHandler(logging.StreamHandler): @@ -636,17 +639,6 @@

        Module exchangelib.util

        return parsed_url.scheme == 'https', parsed_url.netloc.lower(), parsed_url.path -def is_valid_hostname(hostname, timeout): - log.debug('Checking if %s can be looked up in DNS', hostname) - resolver = dns.resolver.Resolver() - resolver.timeout = timeout - try: - resolver.query(hostname) - except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): - return False - return True - - def get_redirect_url(response, allow_relative=True, require_relative=False): # allow_relative=False throws RelativeRedirect error if scheme and hostname are equal to the request # require_relative=True throws RelativeRedirect error if scheme and hostname are not equal to the request @@ -794,6 +786,11 @@

        Module exchangelib.util

        except TokenExpiredError as e: log.debug('Session %s thread %s: OAuth token expired; refreshing', session.session_id, thread_id) r = DummyResponse(url=url, headers={'TokenExpiredError': e}, request_headers=headers, status_code=401) + except KeyError as e: + if e.args[0] != 'www-authenticate': + raise + log.debug('Session %s thread %s: auth headers missing from %s', session.session_id, thread_id, url) + r = DummyResponse(url=url, headers={'KeyError': e}, request_headers=headers) finally: log_vals.update( retry=retry, @@ -1132,49 +1129,38 @@

        Returns

        return False
        -
        -def is_valid_hostname(hostname, timeout) -
        -
        -
        -
        - -Expand source code - -
        def is_valid_hostname(hostname, timeout):
        -    log.debug('Checking if %s can be looked up in DNS', hostname)
        -    resolver = dns.resolver.Resolver()
        -    resolver.timeout = timeout
        -    try:
        -        resolver.query(hostname)
        -    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
        -        return False
        -    return True
        -
        -
        -def is_xml(text) +def is_xml(text, expected_prefix=b'<?xml')
        -

        Helper function. Lightweight test if response is an XML doc

        +

        Lightweight test if response is an XML doc. It's better to be fast than correct here.

        Args

        -

        text:

        +
        +
        text
        +
        The string to check
        +
        expected_prefix
        +
        What to search for in the start if the string
        +
        Expand source code -
        def is_xml(text):
        -    """Helper function. Lightweight test if response is an XML doc
        +
        def is_xml(text, expected_prefix=b'<?xml'):
        +    """Lightweight test if response is an XML doc. It's better to be fast than correct here.
         
             Args:
        -      text:
        +      text: The string to check
        +      expected_prefix: What to search for in the start if the string
         
             """
             # BOM_UTF8 is an UTF-8 byte order mark which may precede the XML from an Exchange server
             bom_len = len(BOM_UTF8)
        +    prefix_len = len(expected_prefix)
             if text[:bom_len] == BOM_UTF8:
        -        return text[bom_len:bom_len + 5] == b'<?xml'
        -    return text[:5] == b'<?xml'
        + prefix = text[bom_len:bom_len + prefix_len] + else: + prefix = text[:prefix_len] + return prefix == expected_prefix
        @@ -1349,6 +1335,11 @@

        Args

        except TokenExpiredError as e: log.debug('Session %s thread %s: OAuth token expired; refreshing', session.session_id, thread_id) r = DummyResponse(url=url, headers={'TokenExpiredError': e}, request_headers=headers, status_code=401) + except KeyError as e: + if e.args[0] != 'www-authenticate': + raise + log.debug('Session %s thread %s: auth headers missing from %s', session.session_id, thread_id, url) + r = DummyResponse(url=url, headers={'KeyError': e}, request_headers=headers) finally: log_vals.update( retry=retry, @@ -2480,7 +2471,6 @@

        Index

      • get_xml_attr
      • get_xml_attrs
      • is_iterable
      • -
      • is_valid_hostname
      • is_xml
      • peek
      • post_ratelimited
      • @@ -2561,7 +2551,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/version.html b/docs/exchangelib/version.html index 3f10ad56..ff967a26 100644 --- a/docs/exchangelib/version.html +++ b/docs/exchangelib/version.html @@ -3,16 +3,16 @@ - + exchangelib.version API documentation - + - + @@ -848,7 +848,7 @@

        -

        Generated by pdoc 0.8.4.

        +

        Generated by pdoc 0.9.1.

        \ No newline at end of file diff --git a/docs/exchangelib/winzone.html b/docs/exchangelib/winzone.html index 1cc27d16..4eaff0eb 100644 --- a/docs/exchangelib/winzone.html +++ b/docs/exchangelib/winzone.html @@ -3,17 +3,17 @@ - + exchangelib.winzone API documentation - - + - + @@ -23,13 +23,13 @@

        Module exchangelib.winzone

        -

        A dict to translate from pytz location name to Windows timezone name. Translations taken from +

        A dict to translate from IANA location name to Windows timezone name. Translations taken from http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml

        Expand source code -
        """ A dict to translate from pytz location name to Windows timezone name. Translations taken from
        +
        """ A dict to translate from IANA location name to Windows timezone name. Translations taken from
         http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml """
         import re
         
        @@ -55,7 +55,7 @@ 

        Module exchangelib.winzone

        for e in to_xml(r.content).find('windowsZones').find('mapTimezones').findall('mapZone'): for location in re.split(r'\s+', e.get('type')): if e.get('territory') == DEFAULT_TERRITORY or location not in tz_map: - # Prefer default territory. This is so MS_TIMEZONE_TO_PYTZ_MAP maps from MS timezone ID back to the + # Prefer default territory. This is so MS_TIMEZONE_TO_IANA_MAP maps from MS timezone ID back to the # "preferred" region/location timezone name. if not location: raise ValueError('Expected location') @@ -64,9 +64,9 @@

        Module exchangelib.winzone

        # This map is generated irregularly from generate_map(). Do not edit manually - make corrections to -# PYTZ_TO_MS_TIMEZONE_MAP instead. We provide this map to avoid hammering the CLDR_WINZONE_URL. +# IANA_TO_MS_TIMEZONE_MAP instead. We provide this map to avoid hammering the CLDR_WINZONE_URL. # -# This list was generated from CLDR_WINZONE_URL version 2019b. +# This list was generated from CLDR_WINZONE_URL version 2020a. CLDR_TO_MS_TIMEZONE_MAP = { 'Africa/Abidjan': ('Greenwich Standard Time', 'CI'), 'Africa/Accra': ('Greenwich Standard Time', 'GH'), @@ -269,10 +269,10 @@

        Module exchangelib.winzone

        'America/Winnipeg': ('Central Standard Time', 'CA'), 'America/Yakutat': ('Alaskan Standard Time', 'US'), 'America/Yellowknife': ('Mountain Standard Time', 'CA'), - 'Antarctica/Casey': ('Singapore Standard Time', 'AQ'), + 'Antarctica/Casey': ('Central Pacific Standard Time', 'AQ'), 'Antarctica/Davis': ('SE Asia Standard Time', 'AQ'), 'Antarctica/DumontDUrville': ('West Pacific Standard Time', 'AQ'), - 'Antarctica/Macquarie': ('Central Pacific Standard Time', 'AU'), + 'Antarctica/Macquarie': ('Tasmania Standard Time', 'AU'), 'Antarctica/Mawson': ('West Asia Standard Time', 'AQ'), 'Antarctica/McMurdo': ('New Zealand Standard Time', 'AQ'), 'Antarctica/Palmer': ('SA Eastern Standard Time', 'AQ'), @@ -529,15 +529,14 @@

        Module exchangelib.winzone

        'Pacific/Wallis': ('UTC+12', 'WF'), } -# Add timezone names used by pytz (which gets timezone names from -# IANA) that are not found in the CLDR. +# Add timezone names used by IANA that are not found in the CLDR. # Use 'noterritory' unless you want to override the standard mapping # (in which case, '001'). # TODO: A full list of the IANA names missing in CLDR can be found with: # -# sorted(set(pytz.all_timezones) - set(CLDR_TO_MS_TIMEZONE_MAP)) +# sorted(set(zoneinfo.available_timezones()) - set(CLDR_TO_MS_TIMEZONE_MAP)) # -PYTZ_TO_MS_TIMEZONE_MAP = dict( +IANA_TO_MS_TIMEZONE_MAP = dict( CLDR_TO_MS_TIMEZONE_MAP, **{ 'Asia/Kolkata': ('India Standard Time', 'noterritory'), @@ -546,9 +545,9 @@

        Module exchangelib.winzone

        } ) -# Reverse map from Microsoft timezone ID to pytz timezone name. Non-CLDR timezone ID's can be added here. -MS_TIMEZONE_TO_PYTZ_MAP = dict( - {v[0]: k for k, v in PYTZ_TO_MS_TIMEZONE_MAP.items() if v[1] == DEFAULT_TERRITORY}, +# Reverse map from Microsoft timezone ID to IANA timezone name. Non-CLDR timezone ID's can be added here. +MS_TIMEZONE_TO_IANA_MAP = dict( + {v[0]: k for k, v in IANA_TO_MS_TIMEZONE_MAP.items() if v[1] == DEFAULT_TERRITORY}, **{ 'tzone://Microsoft/Utc': 'UTC', } @@ -590,7 +589,7 @@

        Args

        for e in to_xml(r.content).find('windowsZones').find('mapTimezones').findall('mapZone'): for location in re.split(r'\s+', e.get('type')): if e.get('territory') == DEFAULT_TERRITORY or location not in tz_map: - # Prefer default territory. This is so MS_TIMEZONE_TO_PYTZ_MAP maps from MS timezone ID back to the + # Prefer default territory. This is so MS_TIMEZONE_TO_IANA_MAP maps from MS timezone ID back to the # "preferred" region/location timezone name. if not location: raise ValueError('Expected location') @@ -623,7 +622,7 @@

        Index

        \ No newline at end of file diff --git a/exchangelib/__init__.py b/exchangelib/__init__.py index 67031272..993971e9 100644 --- a/exchangelib/__init__.py +++ b/exchangelib/__init__.py @@ -16,7 +16,7 @@ from .transport import BASIC, DIGEST, NTLM, GSSAPI, SSPI, OAUTH2, CBA from .version import Build, Version -__version__ = '3.2.1' +__version__ = '3.3.0' __all__ = [ '__version__',