exchangelib.transport API documentation

# Module `exchangelib.transport`

Expand source code

    import logging
    import time

    import requests.auth
    import requests_ntlm
    import requests_oauthlib

    from .errors import UnauthorizedError, TransportError
    from .util import create_element, add_xml_child, xml_to_str, ns_translation, _back_off_if_needed, \
        _retry_after, DummyResponse, CONNECTION_ERRORS

    log = logging.getLogger(__name__)

    # Authentication method enums
    NOAUTH = 'no authentication'
    NTLM = 'NTLM'
    BASIC = 'basic'
    DIGEST = 'digest'
    GSSAPI = 'gssapi'
    SSPI = 'sspi'
    OAUTH2 = 'OAuth 2.0'
    CBA = 'CBA'  # Certificate Based Authentication

    # The auth types that must be accompanied by a credentials object
    CREDENTIALS_REQUIRED = (NTLM, BASIC, DIGEST, OAUTH2)

    AUTH_TYPE_MAP = {
        NTLM: requests_ntlm.HttpNtlmAuth,
        BASIC: requests.auth.HTTPBasicAuth,
        DIGEST: requests.auth.HTTPDigestAuth,
        OAUTH2: requests_oauthlib.OAuth2,
        CBA: None,
        NOAUTH: None,
    }
    try:
        import requests_gssapi
        AUTH_TYPE_MAP[GSSAPI] = requests_gssapi.HTTPSPNEGOAuth
    except ImportError:
        # Kerberos auth is optional
        pass
    try:
        import requests_negotiate_sspi
        AUTH_TYPE_MAP[SSPI] = requests_negotiate_sspi.HttpNegotiateAuth
    except ImportError:
        # SSPI auth is optional
        pass

    DEFAULT_ENCODING = 'utf-8'
    DEFAULT_HEADERS = {'Content-Type': 'text/xml; charset=%s' % DEFAULT_ENCODING, 'Accept-Encoding': 'gzip, deflate'}

    def wrap(content, api_version, account_to_impersonate=None, timezone=None):
        """Generate the necessary boilerplate XML for a raw SOAP request. The XML is specific to the server version.
        ExchangeImpersonation allows to act as the user we want to impersonate.

        RequestServerVersion element on MSDN:
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/requestserverversion

        ExchangeImpersonation element on MSDN:
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exchangeimpersonation

        TimeZoneContent element on MSDN:
        https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/timezonecontext

        :param content:
        :param api_version:
        :param account_to_impersonate:  (Default value = None)
        :param timezone:  (Default value = None)
        """
        envelope = create_element('s:Envelope', nsmap=ns_translation)
        header = create_element('s:Header')
        requestserverversion = create_element('t:RequestServerVersion', attrs=dict(Version=api_version))
        header.append(requestserverversion)
        if account_to_impersonate:
            exchangeimpersonation = create_element('t:ExchangeImpersonation')
            connectingsid = create_element('t:ConnectingSID')
            # We have multiple options for uniquely identifying the user. Here's a prioritized list in accordance with
            # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/connectingsid
            for attr, tag in (
                ('sid', 'SID'),
                ('upn', 'PrincipalName'),
                ('smtp_address', 'SmtpAddress'),
                ('primary_smtp_address', 'PrimarySmtpAddress'),
            ):
                val = getattr(account_to_impersonate, attr)
                if val:
                    add_xml_child(connectingsid, 't:%s' % tag, val)
                    break
            exchangeimpersonation.append(connectingsid)
            header.append(exchangeimpersonation)
        if timezone:
            timezonecontext = create_element('t:TimeZoneContext')
            timezonedefinition = create_element('t:TimeZoneDefinition', attrs=dict(Id=timezone.ms_id))
            timezonecontext.append(timezonedefinition)
            header.append(timezonecontext)
        envelope.append(header)
        body = create_element('s:Body')
        body.append(content)
        envelope.append(body)
        return xml_to_str(envelope, encoding=DEFAULT_ENCODING, xml_declaration=True)

    def get_auth_instance(auth_type, **kwargs):
        """Return an *Auth instance suitable for the requests package.

        :param auth_type:
        :param kwargs:
        """
        model = AUTH_TYPE_MAP[auth_type]
        if model is None:
            return None
        if auth_type == GSSAPI:
            # Kerberos auth relies on credentials supplied via a ticket available externally to this library
            return model()
        if auth_type == SSPI:
            # SSPI auth does not require credentials, but can have it
            return model(**kwargs)
        return model(**kwargs)

    def get_service_authtype(service_endpoint, retry_policy, api_versions, name):
        # Get auth type by tasting headers from the server. Only do POST requests. HEAD is too error prone, and some servers
        # are set up to redirect to OWA on all requests except POST to /EWS/Exchange.asmx
        #
        # We don't know the API version yet, but we need it to create a valid request because some Exchange servers only
        # respond when given a valid request. Try all known versions. Gross.
        from .protocol import BaseProtocol
        retry = 0
        wait = 10  # seconds
        t_start = time.monotonic()
        headers = DEFAULT_HEADERS.copy()
        for api_version in api_versions:
            data = dummy_xml(api_version=api_version, name=name)
            log.debug('Requesting %s from %s', data, service_endpoint)
            while True:
                _back_off_if_needed(retry_policy.back_off_until)
                log.debug('Trying to get service auth type for %s', service_endpoint)
                with BaseProtocol.raw_session(service_endpoint) as s:
                    try:
                        r = s.post(url=service_endpoint, headers=headers, data=data, allow_redirects=False,
                                   timeout=BaseProtocol.TIMEOUT)
                        r.close()  # Release memory
                        break
                    except CONNECTION_ERRORS as e:
                        # Don't retry on TLS errors. They will most likely be persistent.
                        total_wait = time.monotonic() - t_start
                        r = DummyResponse(url=service_endpoint, headers={}, request_headers=headers)
                        if retry_policy.may_retry_on_error(response=r, wait=total_wait):
                            wait = _retry_after(r, wait)
                            log.info("Connection error on URL %s (retry %s, error: %s). Cool down %s secs",
                                     service_endpoint, retry, e, wait)
                            retry_policy.back_off(wait)
                            retry += 1
                            continue
                        raise TransportError(str(e)) from e
            if r.status_code not in (200, 401):
                log.debug('Unexpected response: %s %s', r.status_code, r.reason)
                continue
            try:
                auth_type = get_auth_method_from_response(response=r)
                log.debug('Auth type is %s', auth_type)
                return auth_type, api_version
            except UnauthorizedError:
                continue
        raise TransportError('Failed to get auth type from service')

    def get_auth_method_from_response(response):
        # First, get the auth method from headers. Then, test credentials. Don't handle redirects - burden is on caller.
        log.debug('Request headers: %s', response.request.headers)
        log.debug('Response headers: %s', response.headers)
        if response.status_code == 200:
            return NOAUTH
        # Get auth type from headers
        for key, val in response.headers.items():
            if key.lower() == 'www-authenticate':
                # Requests will combine multiple HTTP headers into one in 'request.headers'
                vals = _tokenize(val.lower())
                for v in vals:
                    if v.startswith('realm'):
                        realm = v.split('=')[1].strip('"')
                        log.debug('realm: %s', realm)
                # Prefer most secure auth method if more than one is offered. See discussion at
                # http://docs.oracle.com/javase/7/docs/technotes/guides/net/http-auth.html
                if 'digest' in vals:
                    return DIGEST
                if 'ntlm' in vals:
                    return NTLM
                if 'basic' in vals:
                    return BASIC
        raise UnauthorizedError('No compatible auth type was reported by server')

    def _tokenize(val):
        # Splits cookie auth values
        auth_methods = []
        auth_method = ''
        quote = False
        for c in val:
            if c in (' ', ',') and not quote:
                if auth_method not in ('', ','):
                    auth_methods.append(auth_method)
                auth_method = ''
                continue
            if c == '"':
                auth_method += c
                if quote:
                    auth_methods.append(auth_method)
                    auth_method = ''
                quote = not quote
                continue
            auth_method += c
        if auth_method:
            auth_methods.append(auth_method)
        return auth_methods

    def dummy_xml(api_version, name):
        # Generate a minimal, valid EWS request
        from .services import ResolveNames  # Avoid circular import
        return wrap(content=ResolveNames(protocol=None).get_payload(
            unresolved_entries=[name],
            parent_folders=None,
            return_full_contact_data=False,
            search_scope=None,
            contact_data_shape=None,
        ), api_version=api_version)

## Functions

`def dummy_xml(api_version, name)`
  Expand source code

      def dummy_xml(api_version, name):
          # Generate a minimal, valid EWS request
          from .services import ResolveNames  # Avoid circular import
          return wrap(content=ResolveNames(protocol=None).get_payload(
              unresolved_entries=[name],
              parent_folders=None,
              return_full_contact_data=False,
              search_scope=None,
              contact_data_shape=None,
          ), api_version=api_version)

`def get_auth_instance(auth_type, **kwargs)`

  Return an *Auth instance suitable for the requests package.

  :param auth_type: :param kwargs:

  Expand source code

      def get_auth_instance(auth_type, **kwargs):
          """Return an *Auth instance suitable for the requests package.

          :param auth_type:
          :param kwargs:
          """
          model = AUTH_TYPE_MAP[auth_type]
          if model is None:
              return None
          if auth_type == GSSAPI:
              # Kerberos auth relies on credentials supplied via a ticket available externally to this library
              return model()
          if auth_type == SSPI:
              # SSPI auth does not require credentials, but can have it
              return model(**kwargs)
          return model(**kwargs)

`def get_auth_method_from_response(response)`
  Expand source code

      def get_auth_method_from_response(response):
          # First, get the auth method from headers. Then, test credentials. Don't handle redirects - burden is on caller.
          log.debug('Request headers: %s', response.request.headers)
          log.debug('Response headers: %s', response.headers)
          if response.status_code == 200:
              return NOAUTH
          # Get auth type from headers
          for key, val in response.headers.items():
              if key.lower() == 'www-authenticate':
                  # Requests will combine multiple HTTP headers into one in 'request.headers'
                  vals = _tokenize(val.lower())
                  for v in vals:
                      if v.startswith('realm'):
                          realm = v.split('=')[1].strip('"')
                          log.debug('realm: %s', realm)
                  # Prefer most secure auth method if more than one is offered. See discussion at
                  # http://docs.oracle.com/javase/7/docs/technotes/guides/net/http-auth.html
                  if 'digest' in vals:
                      return DIGEST
                  if 'ntlm' in vals:
                      return NTLM
                  if 'basic' in vals:
                      return BASIC
          raise UnauthorizedError('No compatible auth type was reported by server')

`def get_service_authtype(service_endpoint, retry_policy, api_versions, name)`
  Expand source code

      def get_service_authtype(service_endpoint, retry_policy, api_versions, name):
          # Get auth type by tasting headers from the server. Only do POST requests. HEAD is too error prone, and some servers
          # are set up to redirect to OWA on all requests except POST to /EWS/Exchange.asmx
          #
          # We don't know the API version yet, but we need it to create a valid request because some Exchange servers only
          # respond when given a valid request. Try all known versions. Gross.
          from .protocol import BaseProtocol
          retry = 0
          wait = 10  # seconds
          t_start = time.monotonic()
          headers = DEFAULT_HEADERS.copy()
          for api_version in api_versions:
              data = dummy_xml(api_version=api_version, name=name)
              log.debug('Requesting %s from %s', data, service_endpoint)
              while True:
                  _back_off_if_needed(retry_policy.back_off_until)
                  log.debug('Trying to get service auth type for %s', service_endpoint)
                  with BaseProtocol.raw_session(service_endpoint) as s:
                      try:
                          r = s.post(url=service_endpoint, headers=headers, data=data, allow_redirects=False,
                                     timeout=BaseProtocol.TIMEOUT)
                          r.close()  # Release memory
                          break
                      except CONNECTION_ERRORS as e:
                          # Don't retry on TLS errors. They will most likely be persistent.
                          total_wait = time.monotonic() - t_start
                          r = DummyResponse(url=service_endpoint, headers={}, request_headers=headers)
                          if retry_policy.may_retry_on_error(response=r, wait=total_wait):
                              wait = _retry_after(r, wait)
                              log.info("Connection error on URL %s (retry %s, error: %s). Cool down %s secs",
                                       service_endpoint, retry, e, wait)
                              retry_policy.back_off(wait)
                              retry += 1
                              continue
                          raise TransportError(str(e)) from e
              if r.status_code not in (200, 401):
                  log.debug('Unexpected response: %s %s', r.status_code, r.reason)
                  continue
              try:
                  auth_type = get_auth_method_from_response(response=r)
                  log.debug('Auth type is %s', auth_type)
                  return auth_type, api_version
              except UnauthorizedError:
                  continue
          raise TransportError('Failed to get auth type from service')

`def wrap(content, api_version, account_to_impersonate=None, timezone=None)`

  Generate the necessary boilerplate XML for a raw SOAP request. The XML is specific to the server version. ExchangeImpersonation allows to act as the user we want to impersonate.

  RequestServerVersion element on MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/requestserverversion

  ExchangeImpersonation element on MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exchangeimpersonation

  TimeZoneContent element on MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/timezonecontext

  :param content: :param api_version: :param account_to_impersonate: (Default value = None) :param timezone: (Default value = None)

  Expand source code

      def wrap(content, api_version, account_to_impersonate=None, timezone=None):
          """Generate the necessary boilerplate XML for a raw SOAP request. The XML is specific to the server version.
          ExchangeImpersonation allows to act as the user we want to impersonate.

          RequestServerVersion element on MSDN:
          https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/requestserverversion

          ExchangeImpersonation element on MSDN:
          https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/exchangeimpersonation

          TimeZoneContent element on MSDN:
          https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/timezonecontext

          :param content:
          :param api_version:
          :param account_to_impersonate:  (Default value = None)
          :param timezone:  (Default value = None)
          """
          envelope = create_element('s:Envelope', nsmap=ns_translation)
          header = create_element('s:Header')
          requestserverversion = create_element('t:RequestServerVersion', attrs=dict(Version=api_version))
          header.append(requestserverversion)
          if account_to_impersonate:
              exchangeimpersonation = create_element('t:ExchangeImpersonation')
              connectingsid = create_element('t:ConnectingSID')
              # We have multiple options for uniquely identifying the user. Here's a prioritized list in accordance with
              # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/connectingsid
              for attr, tag in (
                  ('sid', 'SID'),
                  ('upn', 'PrincipalName'),
                  ('smtp_address', 'SmtpAddress'),
                  ('primary_smtp_address', 'PrimarySmtpAddress'),
              ):
                  val = getattr(account_to_impersonate, attr)
                  if val:
                      add_xml_child(connectingsid, 't:%s' % tag, val)
                      break
              exchangeimpersonation.append(connectingsid)
              header.append(exchangeimpersonation)
          if timezone:
              timezonecontext = create_element('t:TimeZoneContext')
              timezonedefinition = create_element('t:TimeZoneDefinition', attrs=dict(Id=timezone.ms_id))
              timezonecontext.append(timezonedefinition)
              header.append(timezonecontext)
          envelope.append(header)
          body = create_element('s:Body')
          body.append(content)
          envelope.append(body)
          return xml_to_str(envelope, encoding=DEFAULT_ENCODING, xml_declaration=True)